initial release

This commit is contained in:
Gaëtan Renaudeau 2016-12-02 10:03:51 +01:00
commit 4600bcedd0
290 changed files with 46551 additions and 0 deletions

3
.babelrc Executable file
View File

@ -0,0 +1,3 @@
{
"presets": ["es2015", "stage-1", "react"]
}

53
.eslintrc.json Executable file
View File

@ -0,0 +1,53 @@
{
"parser": "babel-eslint",
"plugins": [
"react",
"flowtype"
],
"globals": {
"ReactClass": true
},
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:flowtype/recommended"
],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module"
},
"settings": {
"flowtype": {
"onlyFilesWithFlowAnnotation": false
}
},
"rules": {
"strict": 0,
"flowtype/define-flow-type": 1,
"flowtype/use-flow-type": 1,
"indent": [
"error",
2
],
"no-console": [
2,
{ "allow": ["warn"] }
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
]
}
}

9
.github/ISSUE_TEMPLATE.md vendored Executable file
View File

@ -0,0 +1,9 @@
### *feature request* or *bug report*?
<!-- If you report a bug: -->
### Expected behavior
### Actual behavior
### Steps to reproduce the behavior

9
.github/PULL_REQUEST_TEMPLATE.md vendored Executable file
View File

@ -0,0 +1,9 @@
<!-- Thanks for submitting a pull request! Please provide enough information so that others can review your pull request. The two fields below are mandatory. -->
**Summary**
<!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? -->
**Test plan**
<!-- Demonstrate the code is solid. Please provide if possible an example and also make sure your changes is covered by tests -->

11
.gitignore vendored Executable file
View File

@ -0,0 +1,11 @@
node_modules
coverage
build
.DS_Store
.env
npm-debug.log
*.tmp
packages/*/lib
packages/gl-react/gl-react.js
packages/gl-react-dom/gl-react-dom.js
tests/node_modules_to_test/

67
CONTRIBUTING.md Executable file
View File

@ -0,0 +1,67 @@
# CONTRIBUTING
Contributions are always welcome, no matter how large or small.
## Setup
```sh
git clone https://github.com/gre/gl-react.git
npm run install-all
npm run build
```
## Building
```sh
npm run build
```
You must run this each time you modify one of the packages/ of the library.
It rebuild the lib/ folders and will copy the packages into the projects.
## Testing
```sh
npm test
```
if you need to regenerate the snapshots:
```sh
npm run test-rewrite-snapshots
```
typecheck:
```sh
npm run flow
```
Finally, Please check that ALL examples of the cookbook are working correctly.
## Cookbook
**Run it**
```sh
cd cookbook
npm start
```
**Deploy** (only `gre` can do this at the moment!)
```sh
npm run deploy-cookbook
```
## Clean up and reinstall everything
```sh
npm run clean-all
npm run install-all
```
## License
By contributing to gl-react, you agree that your contributions will be licensed
under its [MIT license](LICENSE).

19
LICENSE Executable file
View File

@ -0,0 +1,19 @@
The MIT License (MIT)
Copyright (c) 2016 <renaudeau.gaetan@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

130
README.md Executable file
View File

@ -0,0 +1,130 @@
> **NB:** This is gl-react v3. For gl-react v2, please see [ProjectSeptemberInc/gl-react](https://github.com/ProjectSeptemberInc/gl-react).
> [gre/gl-react](https://github.com/gre/gl-react) repository is a complete rewrite of [ProjectSeptemberInc/gl-react](https://github.com/ProjectSeptemberInc/gl-react) library (gl-react v2).
It plans to be the gl-react v3.
We keep both repository at same time because (1) this work is not yet finished and (2) this repository is now a "multi libraries" repository (it's just easier to maintain that way).
## v3 alpha: development in progress
- [x] gl-react, universal implementation
- [x] gl-react-dom, DOM implementation
- [x] gl-react-headless, Node.js implementation
- [x] tests: 100% test coverage!
- [ ] gl-react-native, React Native implementation
**The main remaining work of v3 is the React Native implementation**:
The long term plan is to rely on [Exponent's WebGL implementation](https://docs.getexponent.com/versions/v11.0.0/sdk/gl-view.html) to implement the WebGL layer. The implementation is still very young and experimental (only implement a subset of WebGL), but as soon as this implementation guarantees a good conformance, the library should just work! **I encourage everyone to contribute to make Exponent WebGL implementation robust**, independently from the library you use at the end (Three.js / Pixi.js / regl / gl-react / whatever!).
Here is the parts we would like to focus on solving in that implementation:
- Support for framebuffers. This is fundamental for gl-react.
- interoperability with React Native Image `source` prop (basically should support same format as a input for `gl.texImage2D`)
- The WebGL implementation should be a standalone implementation that we can depend as a library (shouldn't requires you to use Exponent if you just use React Native).
- interoperability with more "pixel sources" (Video, Camera, ...)
**Other remaining topics:**
- Flow type support: we are waiting the next flow version that should bring WebGL type support: https://github.com/facebook/flow/pull/2764 .
<img width="32" alt="icon" src="https://cloud.githubusercontent.com/assets/211411/9813786/eacfcc24-5888-11e5-8f9b-5a907a2cbb21.png"> gl-react
========
`gl-react` is a [React](https://facebook.github.io/react/) library to write and compose WebGL shaders. *Implement complex effects by composing React components.*
This universal library must be coupled with one of the concrete implementations:
- [`gl-react-dom`](packages/gl-react-dom/) for React DOM (web using WebGL).
- **unfinished** [`gl-react-native`](packages/gl-react-native/) for React Native (iOS/Android via OpenGL).
- [`gl-react-headless`](packages/gl-react-headless/) for Node.js (used for testing for now)
## Links
- [Cookbook, examples, API](https://gl-react-cookbook.surge.sh)
- Chat [#gl-react on reactiflux](https://discordapp.com/channels/102860784329052160/106102146109325312)
## Gist
```js
import React from "react";
import { Shaders, Node, GLSL } from "gl-react";
const shaders = Shaders.create({
helloBlue: {
frag: GLSL`
precision highp float;
varying vec2 uv;
uniform float blue;
void main() {
gl_FragColor = vec4(uv.x, uv.y, blue, 1.0);
}`
}
});
class HelloBlue extends React.Component {
render() {
const { blue } = this.props;
return <Node
shader={shaders.helloBlue}
uniforms={{ blue }}
/>;
}
}
```
import the correct implementation,
```js
import {Surface} from "gl-react-dom"; // for React DOM
import {Surface} from "gl-react-native"; // for React Native
import {Surface} from "gl-react-headless"; // for Node.js!
```
and this code...
```js
<Surface width={300} height={300}>
<HelloBlue blue={0.5} />
</Surface>
```
...renders:
![](https://cloud.githubusercontent.com/assets/211411/9386550/432492c6-475c-11e5-9328-f3d5187298c1.jpg)
## Features
- **React, VDOM and immutable paradigm**: OpenGL is a low level imperative and mutable API. This library takes the best of it and exposes it in an immutable, descriptive way with React.
- **React lifecycle** allows partial GL re-rendering. Only a React Component update will trigger a redraw. Each Node holds a framebuffer state that get redrawn when component updates, then scheduling a Surface reflow.
- **Developer experience**
- React DevTools works like on DOM and allows you to inspect and debug your stack of effects.
- **Uniform bindings**: bindings from JavaScript objects to OpenGL GLSL language types (bool, int, float, vec2, vec3, vec4, mat2, mat3, mat4, sampler2D...)
- An **extensible texture loader** that allows to support any content that goes in the shader as a sampler2D texture.
- support for images
- support for videos (currently `gl-react-dom`)
- support for canvas (`gl-react-dom`)
- **flowtype** support.
- headless tests with Jest. We have reached 99.9% test coverage!
- Modular, Composable, Sharable. Write shaders once into components that you re-use everywhere! At the end, users don't need to write shaders.
## Atom nice GLSL highlighting
If you are using Atom Editor, you can have JS inlined GLSL syntax highlighted.
![](https://cloud.githubusercontent.com/assets/211411/20623048/0527cce2-b306-11e6-85ee-5020be994c10.png)
**To configure this:**
- add `language-babel` package.
- Configure `language-babel` to add `GLSL:source.glsl` in settings "*JavaScript Tagged Template Literal Grammar Extensions*".
- (Bonus) Add this CSS to your *Atom > Stylesheet*:
```css
/* language-babel blocks */
atom-text-editor::shadow .line .ttl-grammar {
/* NB: designed for dark theme. can be customized */
background-color: rgba(0,0,0,0.3);
}
atom-text-editor::shadow .line .ttl-grammar:first-child:last-child {
display: block; /* force background to take full width only if ttl-grammar is alone in the line. */
}
```

38
TRADEOFFS.md Executable file
View File

@ -0,0 +1,38 @@
# gl-react tradeoffs
## "WebGL is a 2D API"
The library is focused on composing fragment shaders for use-cases like 2D effects on images/videos/...
**That means Vertex shader and Vertex data are currently out of scope, `gl-react` isn't for 3D use-cases yet**. We might provide soon a escape hatch to do arbitrary gl calls in a Node.
## Framebuffers (FBOs)
The library uses **one framebuffer per `<Node>`** and do not re-use FBOs across Node instances.
This allows to implement Node caching (only redraw Node if necessary).
2 exceptions:
- The root `<Node>` do not uses any FBO because it directly draws on the `<Surface>` canvas.
- The use of `backbuffering` will make a `<Node>` uses 2 FBOs: the framebuffer and the backbuffer, each draw will make them swap.
## Surface and Node redraw
In order to make redraw efficient, `gl-react` have 2 phases: the `redraw()` phase and the `flush()` phase (reflecting the respective methods available both on `Surface` and `Node`). In short:
- **redraw() phase** sets a dirty flag to a Node and all its "dependents" (other nodes, buses, surface). *redraws happen generally bottom-up to the Surface.*
- **flush() phase** draws all nodes that have the redraw flag. *draws happens top-down from the Surface.*
`redraw()` is directly hooked to React update lifecycle (re-rendering a Node will calls `redraw()` for you).
To make this system efficient, **the flush() is by default asynchronous**, i.e. `redraw()` means scheduling a new gl draw.
Surface have a main loop that runs at 60 fps and call `flush()`. This is very efficient because if Surface does not have the redraw flag, `flush()` does nothing.
> If you want to make a `<Node>` synchronously flushing the drawing each time it renders, you can still use the `sync` prop (see in the example "`GameOfLife`").
## `<Bus>`, a way to share computation
[gl-react used to automatically factorize the duplicates elements of your tree](http://greweb.me/2016/06/glreactconf/), **it has been decided to remove this feature**
in order to make you fully in control.
*This was actually a pain to implement it right, a premature optimization that can have some slower performance.*
The new gl-react embraces more the React paradigm.
There is a new equivalent way to express a Graph (and share computation): **using a `<Bus>`**.

8
cookbook-rn/.babelrc Executable file
View File

@ -0,0 +1,8 @@
{
"presets": ["react-native-stage-0/decorator-support"],
"env": {
"development": {
"plugins": ["transform-react-jsx-source"]
}
}
}

3
cookbook-rn/.gitignore vendored Executable file
View File

@ -0,0 +1,3 @@
node_modules/**/*
.exponent/*
npm-debug.*

BIN
cookbook-rn/5EOyTDQ.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

28
cookbook-rn/HelloGL.js Executable file
View File

@ -0,0 +1,28 @@
//@flow
import React, {
Component,
} from "react";
import {GLSL, Node, Shaders} from "gl-react";
import {Surface} from "gl-react-native";
const shaders = Shaders.create({
helloGL: {
frag: GLSL`
precision highp float;
varying vec2 uv;
void main() {
gl_FragColor = vec4(uv.x, uv.y, 0.5, 1.0);
}
`
}
});
export default class GLExample1 extends Component {
render () {
return (
<Surface width={100} height={100}>
<Node shader={shaders.helloGL} />
</Surface>
);
}
}

34
cookbook-rn/HelloTexture.js Executable file
View File

@ -0,0 +1,34 @@
//@flow
import React, {
Component,
} from "react";
import {GLSL, Node, Shaders} from "gl-react";
import {Surface} from "gl-react-native";
const shaders = Shaders.create({
helloTexture: {
frag: GLSL`
precision highp float;
varying vec2 uv;
uniform sampler2D t;
void main() {
gl_FragColor = texture2D(t, uv);
}
`
}
});
export default class GLExample1 extends Component {
render () {
return (
<Surface width={100} height={100}>
<Node
shader={shaders.helloTexture}
uniforms={{
t: require("./5EOyTDQ.jpg"),
}}
/>
</Surface>
);
}
}

21
cookbook-rn/exp.json Executable file
View File

@ -0,0 +1,21 @@
{
"name": "glrn-ex",
"description": "An empty new project",
"slug": "glrn-ex",
"sdkVersion": "11.0.3",
"version": "1.0.0",
"orientation": "portrait",
"primaryColor": "#cccccc",
"iconUrl": "https://s3.amazonaws.com/exp-brand-assets/ExponentEmptyManifest_192.png",
"notification": {
"iconUrl": "https://s3.amazonaws.com/exp-us-standard/placeholder-push-icon-blue-circle.png",
"color": "#000000"
},
"loading": {
"iconUrl": "https://s3.amazonaws.com/exp-brand-assets/ExponentEmptyManifest_192.png",
"hideExponentText": false
},
"packagerOpts": {
"assetExts": ["ttf", "mp4"]
}
}

32
cookbook-rn/main.js Executable file
View File

@ -0,0 +1,32 @@
import Exponent from "exponent";
import React from "react";
import {
StyleSheet,
Text,
View,
} from "react-native";
import HelloGL from "./HelloGL";
import HelloTexture from "./HelloTexture";
class App extends React.Component {
render() {
return (
<View style={styles.container}>
<HelloGL />
<HelloTexture />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
},
});
Exponent.registerRootComponent(App);

15
cookbook-rn/package.json Executable file
View File

@ -0,0 +1,15 @@
{
"name": "glrn-ex",
"version": "0.0.0",
"description": "Hello Exponent!",
"author": null,
"private": true,
"main": "main.js",
"dependencies": {
"exponent": "^11.0.3-rc.1",
"gl-react": "file:../packages/gl-react",
"gl-react-native": "file:../packages/gl-react-native",
"react": "~15.3.2",
"react-native": "github:exponentjs/react-native#sdk-11.0.2"
}
}

3
cookbook/.babelrc Executable file
View File

@ -0,0 +1,3 @@
{
"presets": ["react-app"]
}

7
cookbook/.flowconfig Executable file
View File

@ -0,0 +1,7 @@
[ignore]
[include]
[libs]
[options]

13386
cookbook/API.json Executable file

File diff suppressed because it is too large Load Diff

21
cookbook/DocIntro.md Executable file
View File

@ -0,0 +1,21 @@
gl-react API documentation
==========================
`gl-react` is a [React](https://facebook.github.io/react/) library to write and compose WebGL shaders.
This universal library must be coupled with one of the concrete implementations:
- [`gl-react-dom`](https://github.com/gre/gl-react/tree/master/packages/gl-react-dom/) for React DOM (web using WebGL).
- [`gl-react-native`](https://github.com/gre/gl-react/tree/master/packages/gl-react-native/) for React Native (iOS/Android via OpenGL).
- [`gl-react-headless`](https://github.com/gre/gl-react/tree/master/packages/gl-react-headless/) for Node.js (used for testing for now)
[![](https://cloud.githubusercontent.com/assets/211411/9386550/432492c6-475c-11e5-9328-f3d5187298c1.jpg)](/hellogl)
```js
<Surface width={300} height={200}>
<Node shader={shaders.helloGL} />
</Surface>
```
There are two primitive components: [Surface](#surface) and [Node](#node).

45
cookbook/package.json Executable file
View File

@ -0,0 +1,45 @@
{
"name": "gl-react-cookbook",
"version": "3.0.0-alpha",
"private": true,
"devDependencies": {
"babel-preset-react-app": "^1.0.0",
"raw-loader": "^0.5.1",
"react-scripts": "0.6.1"
},
"cookbook": {
"githubprefix": "https://github.com/gre/gl-react/tree/master/cookbook/"
},
"dependencies": {
"animated": "^0.1.3",
"github-slugger": "^1.1.1",
"gl-react": "file:../packages/gl-react",
"gl-react-dom": "file:../packages/gl-react-dom",
"gl-texture2d": "^2.0.12",
"glsl-transitions": "^2016.10.24",
"hoist-non-react-statics": "^1.2.0",
"lodash": "^4.17.2",
"ndarray": "^1.0.18",
"ndarray-ops": "^1.2.2",
"prism-theme-one-dark": "^1.0.0",
"prismjs": "^1.5.1",
"raf": "^3.3.0",
"react": "^15.4.0",
"react-addons-perf": "^15.4.0",
"react-color": "^2.4.0",
"react-dom": "^15.4.0",
"react-json2d": "^0.1.0",
"react-motion": "^0.4.5",
"react-prism": "^4.0.0",
"react-router": "^2.8.1",
"react-sidebar": "^2.2.1",
"remark": "^6.2.0",
"remark-react": "^3.1.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"eject": "react-scripts eject",
"generate-examples": "cd src/examples; ./gen.sh 1> index.js"
}
}

BIN
cookbook/public/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

13
cookbook/public/index.html Executable file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,700" rel="stylesheet">
<title>gl-react cookbook</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

236
cookbook/src/App.css Executable file
View File

@ -0,0 +1,236 @@
a {
color: inherit;
}
a:hover, a:focus {
color: inherit;
}
.menu {
background: #fff;
padding: 10px;
width: 280px;
}
.menu ul {
margin: 0;
padding: 0;
}
.menu li {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 4px;
}
.menu li a {
text-decoration: none;
}
.menu li .example-link.active {
opacity: 0.5;
}
.menu li .example-link strong {
text-decoration: underline;
}
.menu li .example-link.active strong {
text-decoration: none;
}
.App {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
}
.App > header {
height: 50px;
display: flex;
flexDirection: row;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #000;
position: relative;
}
.App > header .sidebar-opener {
font-size: 32px;
padding: 0px 15px;
cursor: pointer;
user-select: none;
text-decoration: none;
}
.App > header .inspector-opener {
font-size: 32px;
padding: 0px 15px;
cursor: pointer;
user-select: none;
text-decoration: none;
}
.App > header .logo {
margin-left: 20px;
text-decoration: none;
font-weight: bold;
font-size: 20px;
white-space: nowrap;
}
.App > header .logo .t1 {
color: #E24;
}
.App > header .logo .t2 {
color: #000;
}
.App > header .logo .t3 {
color: #E24;
}
.App > header .logo .v {
font-weight: normal;
font-size: 0.6em;
margin-left: 10px;
color: #333;
}
.App > header .logo img {
height: 22px;
vertical-align: middle;
margin-right: 10px;
}
.App > header nav {
margin: 0 20px;
}
.App > header nav a {
margin: 0 4px;
padding: 0 4px;
}
.App > header h1 {
font-size: 1.4em;
font-weight: normal;
margin: 0;
padding: 0;
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
text-align: center;
}
.App > header select {
margin: 0 20px;
}
a.left, a.right {
font-size: 1.6em;
padding: 0.6em 0.4em;
background: #fff;
color: #000;
text-decoration: none;
opacity: 0.2;
cursor: pointer;
user-select: none;
}
a.left:hover, a.right:hover {
opacity: 1;
}
a.left {
position: absolute;
top: 50%;
left: 0;
}
a.right {
position: absolute;
top: 50%;
right: 0;
}
.App .container {
flex: 1;
padding: 10px 40px;
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-around;
flex-wrap: wrap;
background: #111;
overflow: auto;
}
.App .container .source {
font-size: 12px;
width: 600px;
overflow: auto;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.App .container .source .viewsource {
color: #fff;
text-align: right;
opacity: 0.8;
padding: 4px 0;
}
.App .container .example {
min-height: 100%;
display: flex;
flex-direction: column;
justify-content: space-around;
user-select: none;
}
.desc {
font-size: 1.1em;
text-align: center;
color: #666;
white-space: pre-line;
display: inline-block;
padding: 10px 0;
}
.desc em {
}
.desc pre {
display: inline-block;
text-align: left;
}
.desc blockquote {
padding: 0;
margin: 0;
padding-left: 20px;
text-align: left;
border-left: 2px solid #333;
opacity: 0.5;
}
.desc strong {
color: #999;
}
.desc p {
margin: 0;
}
.App .container .rendering {
border: 1px solid #000;
display: inline-block;
background: #eee url(https://i.imgur.com/RE6MGrd.png) repeat;
background-size: 20px 20px;
line-height: 0px;
font-family: sans-serif;
font-size: 14px;
align-self: center;
}
.App .container .toolbox {
background: #fff;
display: flex;
flex-direction: column;
align-items: stretch;
padding-bottom: 10px;
}
.App .container .toolbox h3 {
padding: 0;
margin: 4px 0;
}
.App .container .toolbox > * {
padding: 5px 10px;
display: flex;
flex-direction: column;
align-items: stretch;
}
.App .container .example .buttons {
text-align: center;
padding: 1.8em;
background: #fff;
}
.App .container .example .buttons button {
font-size: 1.4em;
}
.inspector {
width: 600px;
height: 100%;
display: flex;
}

212
cookbook/src/App.js Executable file
View File

@ -0,0 +1,212 @@
//@flow
import React, { PureComponent, Component, PropTypes } from "react";
import { findDOMNode } from "react-dom";
import "./App.css";
import {Link} from "react-router";
import logopng from "./logo.png";
import Code from "./Code";
import Sidebar from "react-sidebar";
import Inspector from "./Inspector";
import pkg from "../package.json";
function triggerLink (linkRef) {
const dom = linkRef && findDOMNode(linkRef);
if (dom) dom.click();
}
const lenseSidebar = ({ location: {query: {menu, inspector}} }) =>
({
menu,
inspector,
});
class MenuContext extends PureComponent {
props: {
examples: Array<*>,
menu: bool,
inspector: bool,
currentExample: ?Object,
};
render() {
const { examples, menu, inspector } = this.props;
return <div>
<h3>{examples.length} Examples</h3>
<ul>
{examples.map((ex, i) =>
<li key={ex.path}>
<Link
to={{ pathname: ex.path, query: { menu, inspector } }}
activeClassName="active"
className="example-link">
<strong>{ex.path}</strong>
&nbsp;
<span>{ex.title}</span>
</Link>
</li>)}
</ul>
</div>;
}
}
class Header extends Component {
render() {
const {
currentExample,
toToggleMenu,
toToggleInspector,
} = this.props;
return <header>
{ currentExample
? <Link to={toToggleMenu} className="sidebar-opener"></Link>
: null }
<Link to="/" className="logo">
<img alt="" src={logopng} />
<span className="t1">gl</span>
<span className="t2">-</span>
<span className="t3">react</span>
<span className="v">{pkg.version}</span>
</Link>
<nav>
<Link to="/api">
API
</Link>
<Link to={
currentExample && currentExample.path==="hellogl"
? "/hellogl?menu=true"
: "/hellogl"
}>
Examples
</Link>
<a href="http://github.com/gre/gl-react">
Github
</a>
</nav>
<h1>
{currentExample && currentExample.title}
</h1>
{ currentExample
? <Link to={toToggleInspector} className="inspector-opener"></Link>
: null }
</header>;
}
}
class App extends Component {
static contextTypes = {
router: PropTypes.object.isRequired,
};
componentDidMount() {
document.addEventListener("keydown", this.keydown, false);
}
componentWillUnmount() {
document.removeEventListener("keydown", this.keydown);
}
keydown = (e: KeyboardEvent) => {
switch(e.keyCode) {
case 37: return e.metaKey && e.shiftKey && triggerLink(this.refs.left);
case 39: return e.metaKey && e.shiftKey && triggerLink(this.refs.right);
default:
}
};
render() {
const { children, location, routes } = this.props;
const { menu, inspector } = lenseSidebar(this.props);
const currentExample = routes[1].isExample ? routes[1] : null;
const {LeftSidebar} = routes[1];
const menuOpened = (LeftSidebar && !currentExample) || !!menu;
const inspectorOpened = !!currentExample && !!inspector;
const examples = this.props.route.childRoutes;
const index = examples.indexOf(currentExample);
let prev = examples[index - 1];
if (prev && !prev.isExample) prev = null;
let next = examples[index + 1];
if (next && !next.isExample) next = null;
return (
<Sidebar
docked={inspectorOpened}
pullRight
sidebarClassName="inspector"
sidebar={
inspectorOpened
? <Inspector />
: <span/>
}>
<Sidebar
docked={menuOpened}
contentClassName="App"
sidebarClassName="menu"
sidebar={
currentExample
?
<MenuContext
menu={menu}
inspector={inspector}
currentExample={currentExample}
examples={examples}
/>
:
LeftSidebar ? <LeftSidebar /> : <span/>
}>
<Header
currentExample={currentExample}
toToggleMenu={{
pathname: location.pathname,
query: {
...location.query,
menu: !menuOpened ? true : undefined,
...(!menuOpened ? { inspector: undefined } : null),
}
}}
toToggleInspector={{
pathname: location.pathname,
query: {
...location.query,
inspector: !inspectorOpened ? true : undefined,
...(!inspectorOpened ? { menu: undefined } : null),
}
}}
/>
{ prev
?
<Link ref="left" className="left" to={{
pathname: prev.path,
query: { menu, inspector },
}}></Link>
: null }
{ next
? <Link ref="right" className="right" to={{
pathname: next.path,
query: { menu, inspector },
}}></Link>
: null }
<div className="container">
{children}
{ currentExample
?
<div className="source">
<Code>{currentExample.source}</Code>
<a className="viewsource" href={pkg.cookbook.githubprefix+"examples/"+currentExample.path}>
view source
</a>
</div>
: null }
</div>
</Sidebar>
</Sidebar>
);
}
}
export default App;

13
cookbook/src/Code.css Executable file
View File

@ -0,0 +1,13 @@
pre.cookbook-code a {
color: inherit;
}
pre.cookbook-code .js-template-string-glsl {
background: rgba(0,0,0,0.3);
padding: 4px 8px;
display: block;
max-height: 65vh;
overflow-y: scroll;
}
pre.cookbook-code {
margin: 0;
}

41
cookbook/src/Code.js Executable file
View File

@ -0,0 +1,41 @@
//@flow
import React, { PureComponent } from "react";
import "prismjs";
const {Prism} = window;
import "prismjs/plugins/autolinker/prism-autolinker";
import "prismjs/components/prism-jsx";
import "prismjs/components/prism-glsl";
import "prism-theme-one-dark/prism-onedark.css";
import { PrismCode } from "react-prism";
import "./Code.css";
// add GLSL synthax for GLSL blocks.
Prism.languages.insertBefore("jsx", "string", {
"template-string": {
pattern: /GLSL`(?:\\\\|\\?[^\\])*?`/,
greedy: true,
inside: {
"string": /GLSL`[\n]?|`/,
"js-template-string-glsl": {
pattern: /[^`]*/,
inside: {
rest: Prism.languages.glsl,
},
},
},
}
});
export default class Code extends PureComponent {
props: {
children?: any,
};
render() {
const {children} = this.props;
return (
<pre className="cookbook-code"><PrismCode className="language-jsx">
{children}
</PrismCode></pre>
);
}
}

63
cookbook/src/Dashboard.css Executable file
View File

@ -0,0 +1,63 @@
.dashboard {
color: #eee;
margin: 10px auto;
width: 650px;
}
.dashboard h2 {
font-weight: normal;
margin: 0.6em 0 1.2em 0;
}
.dashboard nav {
margin: 2em 0;
font-size: 1.4em;
letter-spacing: 0.08em;
}
.dashboard nav a {
display: block;
padding: 0.2em 0;
text-decoration: none;
opacity: 0.6;
}
.dashboard nav a:hover {
text-decoration: underline;
opacity: 1;
}
.dashboard .ex {
display: flex;
flex-direction: column;
position: relative;
}
.dashboard .showcode, .dashboard .showunderthehood {
opacity: 0.5;
cursor: pointer;
padding: 10px;
font-size: 10px;
}
.dashboard .showcode:hover, .dashboard .showunderthehood:hover {
opacity: 1;
}
.dashboard .showcode {
text-align: center;
transform: rotate(-90deg) translateX(-100%);
transform-origin: top left;
width: 200px;
position: absolute;
left: 200px;
}
.dashboard .showunderthehood {
text-align: center;
}
.dashboard .ex > header {
display: flex;
flex-direction: row;
}
.dashboard .ex pre {
flex: 1;
}
.dashboard .gl-react-inspector {
background: #fff;
height: 400px;
}

157
cookbook/src/Dashboard.js Executable file
View File

@ -0,0 +1,157 @@
import React, {Component} from "react";
import {Link} from "react-router";
import Code from "./Code";
import {Surface} from "gl-react-dom";
import {Node, Shaders, GLSL, Backbuffer, LinearCopy} from "gl-react";
import timeLoop from "./HOC/timeLoop";
import "./Dashboard.css";
import Inspector from "./Inspector";
const shaders = Shaders.create({
Persistence: {
frag: GLSL`
precision highp float;
varying vec2 uv;
uniform sampler2D t, back;
uniform float persistence;
void main () {
gl_FragColor = vec4(mix(
texture2D(t, uv),
texture2D(back, uv),
persistence
).rgb, 1.0);
}`
},
hello: {
// uniforms are variables from JS. We pipe blue uniform into blue output color
frag: GLSL`
precision highp float;
varying vec2 uv;
uniform float red;
void main() {
gl_FragColor = vec4(red, uv.x, uv.y, 1.0);
}` },
Rotating: {
frag: GLSL`
precision highp float;
varying vec2 uv;
uniform float angle, scale;
uniform sampler2D children;
void main() {
mat2 rotation = mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
vec2 p = (uv - vec2(0.5)) * rotation / scale + vec2(0.5);
gl_FragColor =
p.x < 0.0 || p.x > 1.0 || p.y < 0.0 || p.y > 1.0
? vec4(0.0)
: texture2D(children, p);
}` }
});
const MotionBlur = ({ children: t, persistence }) =>
<Node
shader={shaders.Persistence}
backbuffering
uniforms={{ t, back: Backbuffer, persistence }}
/>;
// We can make a <HelloBlue blue={0.5} /> that will render the concrete <Node/>
class HelloGL extends Component {
props: {
red: number,
};
render() {
const { red } = this.props;
return <Node shader={shaders.hello} uniforms={{ red }} />;
}
}
class Rotate extends Component {
props: {
scale: number,
angle: number,
children: any,
};
render() {
const { angle, scale, children } = this.props;
return <Node shader={shaders.Rotating} uniforms={{ scale, angle, children }} />;
}
}
class Ex1 extends Component {
props: { time: number };
state = {
showCode: false,
showInspector: false,
};
onShowCode = () => {
this.setState({ showCode: true });
};
onShowInspector = () => {
this.setState({ showInspector: true });
};
render() {
const { time } = this.props;
const { showCode, showInspector } = this.state;
const persistence = 0.75 - 0.20 * Math.cos(0.0005 * time);
const red = 0.6 + 0.4 * Math.cos(0.004 * time);
const scale = 0.70 + 0.40 * Math.cos(0.001 * time);
const angle = 2 * Math.PI * (0.5 + 0.5 * Math.cos(0.001 * time));
return (
<div className="ex">
<header>
<Surface width={200} height={200}>
<LinearCopy>
<MotionBlur persistence={persistence}>
<Rotate scale={scale} angle={angle}>
<HelloGL red={red} />
</Rotate>
</MotionBlur>
</LinearCopy>
</Surface>
{ !showCode
? <div onClick={this.onShowCode} className="showcode">SHOW ME THE CODE!</div>
:
<Code>{
` <Surface width={200} height={200}>
<MotionBlur persistence={${persistence.toFixed(2)}}>
<Rotate scale={${scale.toFixed(2)}} angle={${angle.toFixed(2)}}>
<HelloGL red={${red.toFixed(1)}} />
</Rotate>
</MotionBlur>
</Surface>`
}</Code> }
</header>
{ showCode
?
!showInspector
? <div onClick={this.onShowInspector} className="showunderthehood">SHOW ME UNDER THE HOOD!</div>
: <Inspector />
: null }
</div>
);
}
}
const Ex1Loop = timeLoop(Ex1);
export default class Dashboard extends Component {
render() {
return <div className="dashboard">
<h2>
gl-react is a <a href="http://facebook.github.io/react/">React</a> library to write and compose WebGL shaders.
</h2>
<Ex1Loop />
<nav>
<Link to="/hellogl">
Checkout more examples
</Link>
<a href="http://github.com/gre/gl-react">
Explore source code on Github
</a>
<a href="https://discordapp.com/channels/102860784329052160/106102146109325312">
Chat with us, #gl-react on reactiflux
</a>
</nav>
</div>;
}
}

541
cookbook/src/Docs/index.js Executable file
View File

@ -0,0 +1,541 @@
//@flow
// NB This is our custom version of documentation html renderer.
// everything is intentionally inlined here so we can do custom things.
import React, {
Component,
PureComponent,
} from "react";
import GithubSlugger from "github-slugger";
import remark from "remark";
import remarkReactRenderer from "remark-react";
import API from "../../API.json";
import "./style.css";
import Code from "../Code";
import DocIntroMD from "raw!../../DocIntro.md";
const paths = {
"Component": "https://facebook.github.io/react/docs/react-component.html",
"WebGLRenderingContext": "https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.14",
"WebGLContextAttributes": "https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.2",
};
API.forEach(doc => {
paths[doc.name] = "#"+slug(doc.name);
});
function getHref (text: string) {
return text in paths ? paths[text] : null;
}
function t (text) {
return <span>{text}</span>;
}
function visit(tree, type, visitor, reverse) {
if (typeof type === "function") {
reverse = visitor;
visitor = type;
type = null;
}
function all(children, parent) {
var step = reverse ? -1 : 1;
var max = children.length;
var min = -1;
var index = (reverse ? max : min) + step;
var child;
while (index > min && index < max) {
child = children[index];
if (child && one(child, index, parent) === false) {
return false;
}
index += step;
}
return true;
}
function one(node, index, parent) {
var result;
index = index || (parent ? 0 : null);
if (!type || node.type === type) {
result = visitor(node, index, parent || null);
}
if (node.children && result !== false) {
return all(node.children, node);
}
return result;
}
one(tree);
}
function rerouteLinks (ast) {
visit(ast, "link", function (node) {
if (node.jsdoc && !node.url.match(/^(http|https|\.)/) && getHref(node.url)) {
node.url = getHref(node.url);
}
});
return ast;
}
function autolink(text) {
var href = getHref(text);
if (href) {
return <a href={href}>{text}</a>;
}
return t(text);
}
function commaList(getHref, items, start, end, sep) {
var res = [];
if (start) {
res.push(t(start));
}
for (var i = 0, iz = items.length; i < iz; ++i) {
res = res.concat(formatType(items[i]));
if (i + 1 !== iz) {
res.push(t(sep || ", "));
}
}
if (end) {
res.push(t(end));
}
return res;
}
function decorate(formatted, str, prefix) {
if (prefix) {
return [t(str)].concat(formatted);
}
return formatted.concat(t(str));
}
function formatType(node) {
var result = [];
if (!node) {
return [t("any")];
}
switch (node.type) {
case "NullableLiteral":
return [t("?")];
case "AllLiteral":
return [t("any")];
case "NullLiteral":
return [t("null")];
case "VoidLiteral":
return [t("void")];
case "UndefinedLiteral":
return [autolink("undefined")];
case "NameExpression":
return [autolink(node.name)];
case "ParameterType":
return [t(node.name + ": ")].concat(formatType(node.expression));
case "TypeApplication":
return formatType(node.expression)
.concat(commaList(getHref, node.applications, "<", ">"));
case "UnionType":
return commaList(getHref, node.elements, "(", ")", " | ");
case "ArrayType":
return commaList(getHref, node.elements, "[", "]");
case "RecordType":
return commaList(getHref, node.fields, "{", "}");
case "FieldType":
if (node.value) {
return [t(node.key + ": ")].concat(formatType(node.value));
}
return [t(node.key)];
case "FunctionType":
result = [t("function (")];
if (node["this"]) {
if (node["new"]) {
result.push(t("new: "));
} else {
result.push(t("this: "));
}
result = result.concat(formatType(node["this"]));
if (node.params.length !== 0) {
result.push(t(", "));
}
}
result = result.concat(commaList(getHref, node.params, "", ")"));
if (node.result) {
result = result.concat([t(": ")].concat(formatType(node.result)));
}
return result;
case "RestType":
return [ "...", formatType(node.expression) ];
case "OptionalType":
return ["optional ", formatType(node.expression) ].concat(
node.default ? t("(default " + node.default + ")") : []);
case "NonNullableType":
return decorate(formatType(node.expression), "!", node.prefix);
case "NullableType":
return decorate(formatType(node.expression), "?", node.prefix);
case "StringLiteralType":
return [<code>{JSON.stringify(node.value)}</code>];
case "NumericLiteralType":
case "BooleanLiteralType":
return [<code>{String(node.value)}</code>];
default:
throw new Error("Unknown type " + node.type);
}
}
function parameter (param, short) {
if (short) {
return (param.type && param.type.type === "OptionalType") ?
"[" + param.name + "]" : param.name;
}
return [param.name+": ", formatType(param.type)];
}
function parameters (section, short) {
if (section.params) {
return ["("].concat(section.params.map((param, i) =>
[i===0?"":", ", parameter(param, short)])).concat([")"]);
}
return "()";
}
function slug (str) {
var slugger = new GithubSlugger();
return slugger.slug(str);
}
const MD = remark().use(remarkReactRenderer);
function md (ast, inline) {
if (inline && ast && ast.children.length && ast.children[0].type === "paragraph") {
ast = {
type: "root",
children: ast.children[0].children.concat(ast.children.slice(1))
};
}
if (ast) ast = rerouteLinks(ast);
if (!ast) return;
const root = MD.stringify(ast);
if (inline) {
return <span className="md">{root.props.children}</span>;
}
return <div className="md">{root}</div>;
}
function signature (section) {
var returns = "";
var prefix = "";
if (section.kind === "class") {
prefix = "new ";
} else if (section.kind !== "function") {
return section.name;
}
if (section.returns) {
returns = [": ", formatType(section.returns[0].type)];
}
return [prefix + section.name, parameters(section), returns];
}
function isReactComponent (section) {
return section.augments && section.augments.some(a => a.name==="Component");
}
function shortSignature (section) {
var prefix = "";
if (section.kind === "class") {
if (isReactComponent(section)) {
const props =
(section.properties||[])
.filter(p => p.type && p.type.type !== "OptionalType")
.map(p => p.name);
const attrs =
props
.filter(name => name!=="children")
.map(name => " "+name+"={..}")
.join("");
const hasChildren = props.some(name => name==="children");
return "<"+section.name+attrs+(hasChildren ? ">\n ...\n</"+section.name+">" : " />");
}
else {
prefix = "new ";
}
} else if (section.kind !== "function") {
return;
}
return [prefix + section.name, parameters(section, true)];
}
function highlight (code) {
return code ? <Code>{code}</Code> : null;
}
class DocSectionList extends PureComponent {
render() {
const { members } = this.props;
return (
<div>
{members.map((member, i) =>
<div key={i} className="member" id={member.namespace}>
<div className="member-title">
<code>
.{signature(member)}
</code>
</div>
<DocSection
section={member}
nested
/>
</div>)}
</div>
);
}
}
class DocSection extends PureComponent {
render() {
const { section, nested } = this.props;
return (
<section id={slug(section.namespace)} className={"section "+(nested?"nested":"")}>
{(!nested || (section.context && section.context.github))
?
<div>
{!nested
?
<h3>
<a href={"#"+slug(section.namespace)}>{section.name}</a>
{section.augments
?
<span className="augments">
&nbsp;extends&nbsp;
{ section.augments
? section.augments.map((tag, i) => <span key={i}>
{i===0 ? "" : ", "}
{autolink(tag.name)}
</span>)
: null }
</span>
: null }
{ section.context && section.context.github
?
<a className="github-link" href={section.context.github}>
{section.context.path}
</a>
: null }
</h3>
: null }
{ !nested
? highlight(shortSignature(section))
: null }
</div>
: null }
{md(section.description)}
{ section.version ? <div>Version: {section.version}</div> : null }
{ section.license ? <div>License: {section.license}</div> : null }
{ section.author ? <div>Author: {section.author}</div> : null }
{ section.copyright ? <div>Copyright: {section.copyright}</div> : null }
{ section.since ? <div>Since: {section.since}</div> : null }
{section.params
?
<div>
<h4>Parameters</h4>
<div>
{section.params.map((param, i) =>
<div key={i}>
<div>
<strong><code>{param.name}</code></strong>&nbsp;
<code className="type">
({formatType(param.type)}
{param.default
? <span>(default <code>{param.default}</code>)</span>
: null }
)</code>
{md(param.description, true)}
</div>
</div>
)}
</div>
</div>
: null }
{section.properties
?
<div>
<h4>
{isReactComponent(section) ? "Props" : "Properties"}
</h4>
<ul className="props">
{section.properties.map((property, i) =>
<li key={i} className="prop">
<strong><code>{property.name}</code></strong>&nbsp;
<code className="type">({formatType(property.type)})</code>
{ property.default
? <span>(default <code>{property.default}</code>)</span>
: null }
{ property.description
? [": ", md(property.description, true)]
: null }
{ property.properties
?
<ul>
{property.properties.map((property, i) =>
<li key={i}>
<code>{property.name}</code>
&nbsp;
{formatType(property.type)}
{property.default
? <span>(default <code>{property.default}</code>)</span>
: null}
{md(property.description)}
</li>)}
</ul>
: null }
</li>)}
</ul>
</div>
: null }
{section.returns
? section.returns.map((ret, i) =>
<div key={i}>
<h4>Returns</h4>
<code>{formatType(ret.type)}</code>
{ ret.description
? md(ret.description, true)
: null }
</div>)
: null }
{section.throws
?
<div>
<h4>Throws</h4>
<ul>
{section.throws.map((throws, i) =>
<li key={i}>
{formatType(throws.type)}
{": "}
{md(throws.description, true)}
</li>)}
</ul>
</div>
: null }
{ section.examples
?
<div>
<h4>Example{section.examples.length>1?"s":""}</h4>
{section.examples.map((example, i) =>
<div key={i}>
{ example.caption ? <p>{md(example.caption)}</p> : null }
{highlight(example.description)}
</div>)}
</div>
: null }
{ section.members.static && section.members.static.length
?
<div>
<h4>Static Members</h4>
<DocSectionList
members={section.members.static}
noun="Static Member"
/>
</div>
: null }
{ section.members.instance && section.members.instance.length
?
<div>
<h4>Instance Members</h4>
<DocSectionList
members={section.members.instance}
noun="Instance Member"
/>
</div>
: null }
{ section.members.events && section.members.events.length
?
<div>
<h4>Events</h4>
<DocSectionList
members={section.members.events}
noun="Event"
/>
</div>
: null }
</section>
);
}
}
class DocBody extends Component {
render() {
return (
<div>
<div className="intro" id="summary">
<DocIntro />
</div>
{API.map((s, i) =>
s.kind !== "note"
? <DocSection key={i} section={s} />
: null)}
</div>
);
}
}
class DocIntro extends Component {
render() {
return MD.process(DocIntroMD).contents;
}
}
class Doc extends PureComponent {
render() {
return (
<div className="documentation">
<DocBody />
</div>
);
}
}
export class DocToc extends Component {
render () {
return (
<div className="documentation-toc">
<a href="#summary">
gl-react
</a>
<ul>
{API.map((doc, i) =>
<li key={i} className={[
"kind-"+doc.kind,
isReactComponent(doc) ? "react-component" : ""
].join(" ")}>
<a href={"#"+slug(doc.namespace)}>
{doc.name}
</a>
</li>)}
</ul>
</div>
);
}
}
export default class Screen extends Component {
render() {
return <Doc />;
}
}

157
cookbook/src/Docs/style.css Executable file
View File

@ -0,0 +1,157 @@
.documentation {
width: 100%;
min-width: 600px;
max-width: 900px;
color: #555;
line-height: 1.3em;
}
.documentation a[href]:hover {
text-decoration: underline;
}
.documentation-toc ul {
padding: 20px 5px;
overflow: visible;
list-style: none;
}
.documentation-toc li {
padding: 0;
overflow: visible;
}
.documentation-toc li a {
padding: 4px 20px;
position: relative;
display: block;
}
.documentation-toc li.react-component a {
color: #f56;
}
.documentation-toc li.react-component a:before {
content: "<";
}
.documentation-toc li.react-component a:after {
content: " />";
}
.documentation-toc li.kind-typedef a {
color: #69c;
}
.documentation-toc li.kind-typedef a:before {
left: 0px;
color: #69c;
font-size: 10px;
content: "type";
margin-right: 4px;
margin-left: -20px;
vertical-align: middle;
}
a:hover {
cursor: pointer;
}
.documentation strong {
color: #333;
}
.documentation .intro {
padding: 20px 40px;
background: #fff;
}
.documentation .section {
padding: 20px 40px;
background: #fff;
margin: 40px 0;
}
.documentation .section.nested {
margin: 0;
padding: 0;
}
.documentation .section h3 {
color: #000;
font-size: 1.8em;
margin: 0.4em 0;
display: flex;
flex-direction: row;
align-items: baseline;
}
.documentation .section h3 .augments {
font-weight: normal;
font-size: 0.6em;
margin-left: 4px;
}
.documentation .section h3 .github-link {
text-align: right;
font-size: 12px;
flex: 1;
font-weight: normal;
}
.documentation .section h4 {
margin: 0;
margin-left: -0.8em;
margin-top: 1em;
margin-bottom: 0.8em;
font-size: 1.4em;
font-weight: normal;
}
.documentation .type {
opacity: 0.7;
}
.documentation .props {
padding: 0;
margin: 0;
list-style: none;
}
.documentation .props li {
padding: 0.4em 0;
}
.documentation .member {
padding: 20px 0;
}
.documentation .member .member-title {
color: #000;
font-weight: bold;
margin-left: -20px;
padding: 1em 0;
}
.documentation .member > .section {
padding-left: 40px;
}
.documentation .md {
}
.documentation .md blockquote {
opacity: 0.5;
margin: 0.2em 0;
padding: 0 0 0 20px;
border-left: 3px solid #666;
}
.documentation .md h1 {
font-size: 1.6em;
}
.documentation .md h2 {
font-size: 1.4em;
}
.documentation .md h3 {
font-size: 1.2em;
}
.documentation .md h4,
.documentation .md h5,
.documentation .md h6 {
font-size: 1em;
}

87
cookbook/src/ExamplePage.js Executable file
View File

@ -0,0 +1,87 @@
import React, {PropTypes, Component} from "react";
const encodeQueryValue = value => JSON.stringify(value);
const decodeQueryValue = value => JSON.parse(value);
const encodeQuery = obj => {
const values = {};
for (let k in obj) {
if (obj.hasOwnProperty(k)) {
values[k] = encodeQueryValue(obj[k]);
}
}
return values;
};
const decodeQuery = query => {
query = { ...query };
for (let k in query) {
if (query.hasOwnProperty(k)) {
try {
query[k] = decodeQueryValue(query[k]);
}
catch (e) {
console.warn(e);
delete query[k];
}
}
}
return query;
};
export default class ExamplePage extends Component {
static contextTypes = {
router: PropTypes.object.isRequired,
};
setToolState = (obj: any) => {
const { location: { pathname, query } } = this.props;
this.context.router.replace({
pathname,
query: { ...query, ...encodeQuery(obj) }
});
};
onChangeField = prop => value => this.setToolState({ [prop]: value });
render() {
const {
route: { path, toolbox, ToolboxFooter, Example, desc, descAfter },
location: { query }
} = this.props;
const props = {
setToolState: this.setToolState,
...Example.defaultProps,
...decodeQuery(query),
};
return <div id={path} className="example">
<div className="desc">{desc}</div>
<div className="rendering">
<Example {...props} />
</div>
{ toolbox
? <div className="toolbox">
{toolbox.map((field, i) =>
<div key={i} className="field">
{ field.title
? <h3>{
typeof field.title==="function"
? field.title(props[field.prop])
: field.title
}</h3>
: null }
{ field.Editor
? <field.Editor
{...field}
value={props[field.prop]}
onChange={this.onChangeField(field.prop)}
/>
: null }
</div>)}
{ ToolboxFooter
? <ToolboxFooter {...props} />
: null}
</div>
: null }
<div className="desc">{descAfter}</div>
</div>;
}
}

50
cookbook/src/HOC/timeLoop.js Executable file
View File

@ -0,0 +1,50 @@
//@flow
import React, { PureComponent } from "react";
import raf from "raf";
import hoistNonReactStatics from "hoist-non-react-statics";
// NB this is only an utility for the examples
export default (
C: ReactClass<*>,
{ refreshRate = 60 }: { refreshRate?: number } = {}
): ReactClass<*> => {
class TL extends PureComponent {
static displayName = `timeLoop(${C.displayName||C.name||""})`;
state: { time: number };
state = {
time: 0,
tick: 0,
};
_r: any;
componentDidMount() {
let startTime: number, lastTime: number;
let interval = 1000 / refreshRate;
lastTime = -interval;
const loop = (t: number) => {
this._r = raf(loop);
if (!startTime) startTime = t;
if (t - lastTime > interval) {
lastTime = t;
this.setState({
time: t - startTime,
tick: this.state.tick + 1,
});
}
};
this._r = raf(loop);
}
componentWillUnmount() {
raf.cancel(this._r);
}
render() {
return <C
{...this.props}
{...this.state}
/>;
}
};
hoistNonReactStatics(TL, C);
return TL;
}

334
cookbook/src/Inspector/index.css Executable file
View File

@ -0,0 +1,334 @@
.gl-react-inspector {
display: flex;
flex-direction: column;
flex: 1;
font-size: 12px;
background: #fff;
color: #000;
}
.gl-react-inspector h2 {
font-weight: normal;
}
.gl-react-inspector .no-surface {
padding: 20px;
}
.gl-react-inspector .no-surface ul {
font-size: 16px;
}
.gl-react-inspector .no-surface li span {
text-decoration: underline;
cursor: pointer;
}
.gl-react-inspector > header {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 4px;
background: #fff;
}
.gl-react-inspector > header label {
margin-right: 5px;
}
.gl-react-inspector .btn {
cursor: pointer;
text-decoration: underline;
user-select: none;
display: inline-block;
text-align: right;
}
.gl-react-inspector .body {
flex: 1;
background: #f3f3f3;
overflow: auto;
display: flex;
position: relative;
flex-direction: column;
}
.gl-react-inspector .body .nodes {
position: relative;
width: 100%;
height: 100%;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4AscDA0JcudXmQAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAABIElEQVR42u3cMQqAQAxE0YkK3v/Ago3gEULIe43lwvLZzqn0Ob/v4/wWd5IcYTUBCAABIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIABmqfx/qbLL5QUg1Xi2fYDe8+0DIAABuAIBIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIACmsQ+wl30A7ANsPt8+AAIQgCsQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJgGvsAe9kHwD7A5vPtAyAAAbgCASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAApql8f4niBWChF2e+DTF+hz42AAAAAElFTkSuQmCC');
background-size: 64px 64px;
}
.gl-react-inspector .body .hook-drawer {
position: absolute;
top: 0;
left: 0;
overflow: visible;
pointer-events: none;
user-select: none;
}
.gl-react-inspector .box {
position: absolute;;
display: flex;
flex-direction: column;
width: 180px;
border-radius: 2px;
box-shadow: 0px 0px 3px rgba(0,0,0,0);
border: 1px solid #eee;
background: #fff;
transition: 0.2s border-color;
}
.gl-react-inspector .box.recent-draw {
border-color: #999;
transition: 0.1s border-color;
}
/*
.gl-react-inspector .box.node header .name {
color: #C30;
}
.gl-react-inspector .box.bus header .name {
color: #093;
}
*/
.gl-react-inspector .box header {
display: flex;
flex-direction: row;
font-size: 12px;
padding: 0px 4px;
margin-bottom: 4px;
line-height: 24px;
cursor: move;
border-bottom: 1px solid #eee;
}
.gl-react-inspector .box header .name {
flex: 1;
font-weight: bold;
word-wrap: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.gl-react-inspector .box header .redraw {
padding-left: 10px;
padding: 0px 6px;
min-width: 30px;
line-height: 24px;
opacity: 0.5;
cursor: pointer;
text-align: right;
}
.gl-react-inspector .box header .redraw:hover {
opacity: 1;
}
.gl-react-inspector .box header .drawCount {
font-size: 10px;
min-width: 20px;
text-align: right;
}
.gl-react-inspector .box .content-html {
max-height: 80px;
overflow: auto;
color: #aaa;
padding: 2px 8px;
font-size: 9px;
white-space: normal;
word-wrap: break-word;
}
.gl-react-inspector .box.minimized {
width: 80px;
}
.gl-react-inspector .box.minimized header {
border-bottom: none;
margin-bottom: -2px;
}
.gl-react-inspector .box.minimized footer {
border-top: none;
margin-top: 0;
}
.gl-react-inspector .box.minimized .redraw,
.gl-react-inspector .box.minimized .content-html {
display: none;
}
.gl-react-inspector .box.minimized .uniform {
line-height: 12px;
}
.gl-react-inspector .box.minimized .uniform .anchor-hook {
height: 12px;
}
.gl-react-inspector .box.minimized .uniform:not(.type-sampler2D):not(.type-array-sampler2D) {
display: none;
}
.gl-react-inspector .box footer {
font-size: 10px;
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: space-between;
padding: 2px 4px;
margin-top: 4px;
border-top: 1px solid #eee;
cursor: pointer;
user-select: none;
position: relative;
}
.gl-react-inspector .box footer .minimize {
opacity: 0;
position: absolute;
left: -12px;
padding: 1px 4px;
}
.gl-react-inspector .box:hover footer .minimize {
opacity: 0.2;
}
.gl-react-inspector .box footer:hover .minimize {
opacity: 1;
}
.gl-react-inspector .box footer .dim {
color: #888;
font-size: 8px;
}
.gl-react-inspector .box footer .mode {
padding-left: 4px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.gl-react-inspector .box .anchor-hook {
position: absolute;
left: -2px;
height: 16px;
}
.gl-react-inspector .box .hook {
position: absolute;
right: 0px;
height: 20px;
}
.gl-react-inspector .uniforms {
display: flex;
flex-direction: column;
overflow: hidden;
}
.gl-react-inspector .uniforms .uniform {
line-height: 18px;
font-size: 11px;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.gl-react-inspector .uniforms .name {
color: #000;
display: inline-block;
padding: 0 4px;
}
.gl-react-inspector .uniforms .value-container {
/*white-space: nowrap;*/
word-break: break-all;
padding-right: 6px;
overflow: hidden;
}
.gl-react-inspector .uniforms .val {
color: #850;
background: #ffc;
padding: 1px 3px;
border: 1px solid #ed9;
}
.gl-react-inspector .uniforms .value.type-sampler2D {
text-align: right;
}
.gl-react-inspector .uniforms .value.type-sampler2D .val {
display: none;
}
.gl-react-inspector .uniforms .value-array .value-array,
.gl-react-inspector .uniforms .value.type-sampler2D {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gl-react-inspector .uniforms .value-array > :first-child:before {
content: "[";
color: #aaa;
}
.gl-react-inspector .uniforms .value-array > :after {
content: ",";
color: #aaa;
}
.gl-react-inspector .uniforms .value-array > :last-child:after {
content: "]";
color: #aaa;
}
.gl-react-inspector .uniforms .meta-info {
color: #ec6;
padding: 0 2px;
}
.gl-react-inspector .preview {
}
.gl-react-inspector .preview {
background: #fff;
position: relative;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
}
.gl-react-inspector .connection circle {
fill: #666;
r: 1;
}
.gl-react-inspector circle.hook,
.gl-react-inspector circle.anchor {
r: 2;
fill: #666;
}
.gl-react-inspector circle.standalone-output {
r: 2;
fill: #000;
opacity: 1;
}
.gl-react-inspector path.connection-line {
opacity: 0.1;
stroke: #000;
fill: none;
stroke-width: 1;
}
.gl-react-inspector .box.grabbed {
border-color: #000;
transition: none;
}
.gl-react-inspector .anchor-group.grabbed circle.hook,
.gl-react-inspector .anchor-group.grabbed circle.anchor {
fill: #000;
}
.gl-react-inspector .anchor-group.grabbed path.connection-line {
stroke: #000;
opacity: 0.3;
}
.gl-react-inspector .preview canvas {
background: #f3f3f3 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAARklEQVRYw+3XwQkAIAwEwYvYf8uxBcE8fMwWEAbuleruDDd6cOXzAAEBAQEBAQEBAQFf2tM/RJIyMSAgICAgICAgICDgZQeYxgVOKu5KXQAAAABJRU5ErkJggg==) repeat;
background-size: 20px 20px;
border: 1px solid #666;
max-width: 100%;
min-height: 64px;
box-sizing: border-box;
image-rendering: pixelated;
}

1476
cookbook/src/Inspector/index.js Executable file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,46 @@
//@flow
import React, { Component } from "react";
import { Shaders, Node, GLSL } from "gl-react";
import { Surface } from "gl-react-dom";
import Animated from "animated";
const shaders = Shaders.create({
cursor: { frag: GLSL`
precision lowp float; varying vec2 uv; uniform vec2 style;
void main() {
float dist = pow(1.0 - distance(style, uv), 8.0);
gl_FragColor = vec4(smoothstep(2.0, 0.2, distance(style, uv)) * vec3(
1.0 * dist + pow(1.0 - distance(style.y, uv.y), 16.0),
0.5 * dist + pow(1.0 - distance(style.y, uv.y), 32.0),
0.2 * dist + pow(1.0 - distance(style.x, uv.x), 32.0)), 1.0);
}` }
});
class Cursor extends Component {
render() {
const { style: { x, y } } = this.props;
return <Node shader={shaders.cursor} uniforms={{ style: [ x, y ] }} />;
}
}
// using "style" is a hack. see https://github.com/animatedjs/animated/issues/45
const AnimatedCursor = Animated.createAnimatedComponent(Cursor);
export default class Example extends Component {
state = {
style: new Animated.ValueXY({ x: 0.5, y: 0.5 })
};
onMouseMove = (e: any) => {
const rect = e.target.getBoundingClientRect();
Animated.spring(this.state.style, {
toValue: {
x: (e.clientX - rect.left) / rect.width,
y: (rect.bottom - e.clientY) / rect.height,
}
}).start();
};
render() {
return (
<Surface width={500} height={500} onMouseMove={this.onMouseMove}>
<AnimatedCursor {...this.state} />
</Surface>
);
}
};

View File

@ -0,0 +1,5 @@
import markdown from "../../markdown";
export const title = "Cursor spring effect with animated";
export const desc = markdown`
We can also use \`Animated\` animation library.
`;

View File

@ -0,0 +1,55 @@
{
"private": true,
"name": "gre-js13k-2014",
"version": "0.0.0",
"description": "",
"scripts": {
"clean": "rm -rf build/; mkdir -p build target",
"compileglsl": "./scripts/compileglslfiles.sh src/shaders build",
"concat": "./scripts/concat.sh > build/build.js",
"minify": "uglifyjs build/build.js -c --screw-ie8 -m -o build/build.min.js",
"nominify": "cp build/build.js build/build.min.js",
"gen": "cp src/target.html target/index.html && cp build/build.min.js target/b.js",
"build": "export NODE_ENV=production; npm run clean && npm run compileglsl && npm run concat && npm run minify && npm run gen && npm run zip",
"build-nominify": "npm run clean && npm run compileglsl && npm run concat && npm run nominify && npm run gen",
"watch": "npm run build-nominify; wr 'npm run build-nominify' src/ scripts/",
"liveserver": "mkdir -p target; cd target; live-server --no-browser",
"zip": "cd target; zip -r ../target.zip .; cd ..; wc -c target.zip",
"start": "budo index.js:bundle.js | garnish"
},
"browserify": {
"transform": [
"babelify",
"glslify"
]
},
"repository": {
"type": "git",
"url": "git://github.com/gre/js13k-2014.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/gre/js13k-2014/issues"
},
"homepage": "https://github.com/gre/js13k-2014",
"devDependencies": {
"babelify": "^6.4.0",
"browserify": "^11.0.1",
"budo": "^5.1.5",
"eslint": "^1.3.0",
"garnish": "^3.2.1",
"glslify": "^2.3.1",
"glslmin": "0.0.0",
"live-server": "^0.8.1",
"uglify-js": "^2.4.24",
"uglifycss": "^0.0.17",
"wr": "^1.3.1"
},
"dependencies": {
"gl-react": "^1.2.5",
"gl-react-inspector": "0.0.2",
"react": "^0.14.0",
"react-dom": "^0.14.0"
}
}

View File

@ -0,0 +1,22 @@
if [ "$#" -ne 2 ]; then
echo "Invalid arguments. Usage: $0 fromDir toDir" >&2;
exit 1;
fi;
if [ "$1" == "$2" ]; then
echo "fromDir and toDir must be different" >&2;
exit 2;
fi;
if [ ! -d "$1" ]; then
echo "fromDir must be a directory" >&2;
exit 3;
fi;
if [ ! -d "$2" ]; then
echo "toDir must be a directory" >&2;
exit 4;
fi;
for glsl in $1/*.frag $1/*.vert; do
name=`basename $glsl`;
cat $glsl | glslmin > $2/$name;
done;

View File

@ -0,0 +1,47 @@
cat src/pre.js
if [ "$NODE_ENV" == "production" ]; then
cat src/env_prod.js
else
cat src/env_dev.js
fi;
# libs
cat src/lib/math.js
cat src/lib/path.js
cat src/lib/asteroids.font.js
cat src/lib/webgl.js
cat src/lib/jsfxr.js
cat src/lib/audio.js
# shaders
cd build;
for glsl in *.frag *.vert; do
name=`echo $glsl | tr '.' '_' | tr '[:lower:]' '[:upper:]'`
cat $glsl | ../scripts/wrapjs.sh $name
echo
done
cd ..;
# game
cat src/setup.js
cat src/state.js
cat src/sounds.js
cat src/input.js
cat src/behaviors.js
cat src/ai.js
cat src/asteroids.js
cat src/asteroidsIncoming.js
cat src/bullets.js
cat src/particles.js
cat src/spaceship.js
cat src/ufo.js
cat src/ui.js
cat src/effects.js
cat src/game.js
cat src/post.js

View File

@ -0,0 +1,7 @@
#!/bin/bash
# usage: cat glslfile | ./wrapjs.sh varname
echo -n "var $1 ='"
perl -p -e 's/\n/\\n/';
echo -ne "';"

View File

@ -0,0 +1,328 @@
/* global
DEBUG
AIrotate: true
AIboost: true
AIshoot: true
AIexcitement: true
spaceship
t dt
asteroids
bullets
W H
dist normAngle
ufos
playingSince
ctx
*/
/*
if (DEBUG) {
/* eslint-disable no-inner-declarations
var AIdebug = [], AIdebugCircle = [];
function drawAIDebug () {
AIdebug.forEach(function (debug, i) {
ctx.save();
ctx.lineWidth = 2;
ctx.fillStyle = ctx.strokeStyle = "hsl("+Math.floor(360*i/AIdebug.length)+",80%,50%)";
ctx.beginPath();
ctx.moveTo(debug[0], debug[1]);
ctx.lineTo(debug[2], debug[3]);
ctx.stroke();
ctx.beginPath();
ctx.arc(debug[0], debug[1], 2, 0, 2*Math.PI);
ctx.fill();
ctx.restore();
});
AIdebugCircle.forEach(function (debug, i) {
ctx.save();
ctx.lineWidth = 2;
ctx.fillStyle = ctx.strokeStyle = "hsl("+Math.floor(360*i/AIdebugCircle.length)+",80%,50%)";
ctx.beginPath();
ctx.arc(debug[0], debug[1], Math.max(0, debug[2] * debug[3]), 0, 2*Math.PI);
ctx.stroke();
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(debug[0], debug[1], debug[3], 0, 2*Math.PI);
ctx.stroke();
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.fillText(debug[2].toFixed(2), debug[0], debug[1]-debug[3]-2);
ctx.restore();
});
}
function clearDebug () {
AIdebug = [];
AIdebugCircle = [];
}
function addDebugCircle (p, value, radius) {
AIdebugCircle.push([ p[0], p[1], value, radius ]);
}
function addDebug (p, v) {
var d = 200;
AIdebug.push([ p[0], p[1], p[0]+(v?d*v[0]:0), p[1]+(v?d*v[1]:0) ]);
}
function addPolarDebug (p, ang, vel) {
var v = [
vel * Math.cos(ang),
vel * Math.sin(ang)
];
addDebug(p, v);
}
/* eslint-enable
}
*/
var closestAsteroidMemory, targetShootMemory, closestAsteroidMemoryT, targetShootMemoryT;
// AI states
function aiLogic (smart) { // set the 3 AI inputs (rotate, shoot, boost)
var i;
// DEBUG && clearDebug();
// first part is data extraction / analysis
//var ax = Math.cos(spaceship[4]);
//var ay = Math.sin(spaceship[4]);
var vel = Math.sqrt(spaceship[2]*spaceship[2]+spaceship[3]*spaceship[3]);
var velAng = Math.atan2(spaceship[3], spaceship[2]);
//var spaceshipVel = [ ax * vel, ay * vel ];
// utilities
function orient (ang) {
var stableAng = normAngle(ang - spaceship[4]);
AIrotate = stableAng < 0 ? -1 : 1;
return stableAng;
}
function move (ang, vel) {
var stableAng = normAngle(ang - spaceship[4]);
var abs = Math.abs(stableAng);
if (abs > Math.PI/2) {
if (vel) AIboost = abs>Math.PI/2-0.4 ? vel>0?-1:1 : 0;
AIrotate = stableAng > 0 ? -1 : 1;
}
else {
if (vel) AIboost = abs<0.4 ? vel<0?-1:1 : 0;
AIrotate = stableAng < 0 ? -1 : 1;
}
}
// take actions to move and stabilize to a point
function moveToPoint (p, minDist) {
var dx = p[0]-spaceship[0];
var dy = p[1]-spaceship[1];
if (dx*dx+dy*dy<minDist*minDist) return;
var tx = dx / 800;
var ty = dy / 800;
var x = tx - spaceship[2];
var y = ty - spaceship[3];
var ang = Math.atan2(y, x);
var dist = length([x, y]);
move(ang, dist);
}
function dot (a,b) { // dot product
return a[0]*b[0] + a[1]*b[1];
}
// a line defined by AB, point is P
function projectPointToLine (p, a, ab) {
var ap = [ p[0]-a[0], p[1]-a[1] ];
var k = dot(ap,ab)/dot(ab,ab);
return [
a[0] + k * ab[0],
a[1] + k * ab[1]
];
}
function moveAwayFromPoint (p, v) {
var spaceshipToP = [
p[0] - spaceship[0],
p[1] - spaceship[1]
];
var ang = Math.atan2(spaceshipToP[1], spaceshipToP[0]);
var dist = length(spaceshipToP);
// DEBUG && addPolarDebug(p, ang, 0.5);
if (v && vel > 0.003 * dist && Math.abs(normAngle(ang - velAng))<Math.PI/3) {
// if there is some velocity, it is still good to "traverse" the point and not brake
// but we need to target a bit more far from the obj vel
var l = length(v);
spaceshipToP[0] += 100 * v[0]/l;
spaceshipToP[1] += 100 * v[1]/l;
ang = Math.atan2(spaceshipToP[1], spaceshipToP[0]);
move(ang, 1);
}
else {
move(ang, -1);
}
}
// danger have [ x, y, rot, vel ]
function moveAwayFromAsteroid (ast) {
var v = [
ast[3] * Math.cos(ast[2]),// - spaceshipVel[0],
ast[3] * Math.sin(ast[2])// - spaceshipVel[1]
];
var p = projectPointToLine(spaceship, ast, v);
var acceptDist = 30 + 10 * ast[5];
var d = dist(p, spaceship);
if (d > acceptDist) return;
//DEBUG && addDebug(p, v);
moveAwayFromPoint(p, v);
}
function predictShootIntersection (bulletVel, pos, target, targetVel) {
// http://gamedev.stackexchange.com/a/25292
var totarget = [
target[0] - pos[0],
target[1] - pos[1]
];
var a = dot(targetVel, targetVel) - bulletVel * bulletVel;
var b = 2 * dot(targetVel, totarget);
var c = dot(totarget, totarget);
var p = -b / (2 * a);
var q = Math.sqrt((b * b) - 4 * a * c) / (2 * a);
var t1 = p - q;
var t2 = p + q;
var t = t1 > t2 && t2 > 0 ? t2 : t1;
return [t, [
target[0] + targetVel[0] * t,
target[1] + targetVel[1] * t
]];
}
var middle = [W/2,H/2];
var closestAsteroid, targetShoot, danger = 0;
var closestAsteroidScore = 0.3, targetShootScore = 0.1;
var incomingBullet, incomingBulletScore = 0;
for (i = 0; i < asteroids.length; ++i) {
var ast = asteroids[i];
// FIXME: take velocity of spaceship into account?
var v = [
ast[3] * Math.cos(ast[2]),
ast[3] * Math.sin(ast[2])
];
var timeBeforeImpact = dot([ spaceship[0]-ast[0], spaceship[1]-ast[1] ],v)/dot(v,v);
var impact = [
ast[0] + timeBeforeImpact * v[0],
ast[1] + timeBeforeImpact * v[1]
];
var distToImpact = dist(spaceship, impact);
var distWithSize = distToImpact - 10 - 10 * ast[5];
var score =
Math.exp(-distWithSize/40) +
Math.exp(-distWithSize/120) +
timeBeforeImpact > 0 ? Math.exp(-timeBeforeImpact/1000) : 0;
if (score > closestAsteroidScore) {
closestAsteroidScore = score;
closestAsteroid = ast;
danger ++;
}
score =
Math.exp(-(ast[5]-1)) *
Math.exp(-distWithSize/200);
if (score > targetShootScore) {
var res = predictShootIntersection(0.3, spaceship, ast, v);
var t = res[0];
var p = res[1];
if (0<p[0] && p[0]<W && 0<p[1] && p[1]<H && t<800) {
targetShoot = p;
targetShootScore = score;
//DEBUG && addDebugCircle(p, score, 20);
}
}
}
for (i = 0; i < bullets.length; ++i) {
var b = bullets[i];
v = b.slice(2);
timeBeforeImpact = dot([ spaceship[0]-b[0], spaceship[1]-b[1] ],v)/dot(v,v);
impact = [
b[0] + timeBeforeImpact * v[0],
b[1] + timeBeforeImpact * v[1]
];
distToImpact = dist(spaceship, impact);
score = Math.exp(-timeBeforeImpact/1000) + 2*Math.exp(-distToImpact/50);
if (100 < timeBeforeImpact &&
timeBeforeImpact < 1000 &&
distToImpact < 40 &&
score > incomingBulletScore) {
incomingBulletScore = score;
incomingBullet = impact;
}
}
for (i = 0; i < ufos.length; ++i) {
var u = ufos[i];
res = predictShootIntersection(0.3, spaceship, u, u.slice(2));
t = res[0];
p = res[1];
targetShoot = p;
}
AIexcitement =
(1 - Math.exp(-asteroids.length/10)) + // total asteroids
(1 - Math.exp(-danger/3)) // danger
;
// Now we implement the spaceship reaction
// From the least to the most important reactions
// Dump random changes
AIshoot = playingSince > 3000 && Math.random() < 0.001*dt*(1-smart);
AIrotate = (playingSince > 1000 && Math.random()<0.002*dt) ?
(Math.random()<0.6 ? 0 : Math.random() < 0.5 ? -1 : 1) : AIrotate;
AIboost = (playingSince > 2000 && Math.random()<0.004*dt) ?
(Math.random()<0.7 ? 0 : Math.random() < 0.5 ? -1 : 1) : AIboost;
// Stay in center area
if (0.1 + smart > Math.random()) moveToPoint(middle, 30);
// Shot the target
if (smart > Math.random()) {
if (targetShoot) {
AIshoot =
Math.abs(orient(Math.atan2(
targetShoot[1] - spaceship[1],
targetShoot[0] - spaceship[0]))) < 0.1 &&
Math.random() < 0.04 * dt;
targetShootMemory = targetShoot;
targetShootMemoryT = t;
}
else {
AIshoot = 0;
}
}
// Avoid dangers
if (smart > Math.random()) {
if (closestAsteroid) {
moveAwayFromAsteroid(closestAsteroid);
closestAsteroidMemory = closestAsteroid;
closestAsteroidMemoryT = closestAsteroid;
}
if (incomingBullet) moveAwayFromPoint(incomingBullet);
}
//DEBUG && targetShoot && addPolarDebug(targetShoot, 0, 0);
//DEBUG && closestAsteroid && addPolarDebug(closestAsteroid, closestAsteroid[2], closestAsteroid[3]);
}

View File

@ -0,0 +1,102 @@
/* global
ctx path W H asteroids:true rotatingLetters incPosition incRotation MOBILE play
Asend AsendFail
*/
// Logic
function randomAsteroidShape (lvl) {
var n = 4 + lvl * 2;
var size = lvl * 10;
var pts = [];
for (var i = 0; i < n; ++i) {
var l = size*(0.4 + 0.6 * Math.random());
var a = 2 * Math.PI * i / n;
pts.push([
l * Math.cos(a),
l * Math.sin(a)
]);
}
return pts;
}
function randomAsteroids () {
asteroids = [];
for (var i=0; i<8; ++i) {
var lvl = Math.floor(1.5 + 3 * Math.random());
asteroids[i] = [
W * Math.random(),
H * Math.random(),
2 * Math.PI * Math.random(),
0.02 + 0.02 * Math.random(),
randomAsteroidShape(lvl),
lvl
];
}
}
function explodeAsteroid (j) {
var aster = asteroids[j];
asteroids.splice(j, 1);
var lvl = aster[5];
if (lvl > 1) {
var nb = Math.round(2+1.5*Math.random());
for (var k=0; k<nb; k++) {
var a = Math.random() + 2 * Math.PI * k / nb;
asteroids.push([
aster[0] + 10 * Math.cos(a),
aster[1] + 10 * Math.sin(a),
a,
0.5 * aster[3],
randomAsteroidShape(lvl-1),
lvl - 1
]);
}
}
}
function sendAsteroid (o) {
rotatingLetters.push(o[7]);
if (Math.abs(Math.cos(o[2])) < o[9]) {
var p = incPosition(o);
var rot = incRotation(o);
var x = Math.max(0, Math.min(p[0], W));
var y = Math.max(0, Math.min(p[1], H));
var vel = (MOBILE ? 0.006 : 0.008) * o[3];
var lvl = o[6];
var shape = o[5];
asteroids.push([ x, y, rot, vel, shape, lvl ]);
play(Asend);
return 1;
}
else {
play(AsendFail);
}
}
/*
function randomInGameAsteroid () {
var a = Math.random() < 0.5;
var b = Math.random() < 0.5;
var lvl = Math.floor(1 + 2 * Math.random() * Math.random());
asteroids.push([
a ? (b?-20:W+20) : W * Math.random(),
!a ? (b?-20:H+20) : H * Math.random(),
2 * Math.PI * Math.random(),
0.02 + 0.02 * Math.random(),
randomAsteroidShape(lvl),
lvl
]);
}
*/
// RENDERING
function drawAsteroid (o) {
ctx.globalAlpha = 0.2;
ctx.strokeStyle = "#f00";
path(o[4]);
ctx.stroke();
}

View File

@ -0,0 +1,259 @@
/* global
GAME_INC_PADDING W H t dt borderLength spaceship incomingObjects player
playingSince randomAsteroidShape lifes dying ctx path MOBILE font helpVisible
*/
function incPosition (o) {
var i = o[0] % borderLength;
var x, y;
var w = W + GAME_INC_PADDING;
var h = H + GAME_INC_PADDING;
if (i<w) {
x = i;
y = 0;
}
else {
i -= w;
if (i < h) {
x = w;
y = i;
}
else {
i -= h;
if (i < w) {
x = w - i;
y = h;
}
else {
i -= w;
x = 0;
y = h - i;
}
}
}
var p = [ -GAME_INC_PADDING/2 + x, -GAME_INC_PADDING/2 + y ];
if (o[10]) {
var dt = t - o[10];
var a = Math.atan2(spaceship[1] - p[1], spaceship[0] - p[0]);
var l = dt * 0.3;
p[0] -= Math.cos(a) * l;
p[1] -= Math.sin(a) * l;
}
return p;
}
function incRotationCenter (o) {
var p = incPosition(o);
var toCenter = Math.atan2(spaceship[1] - p[1], spaceship[0] - p[0]);
return toCenter;
}
function incRotation (o) {
return Math.cos(o[2]) * o[8] + incRotationCenter(o);
//return o[2];
}
var nextCreate = 0;
function maybeCreateInc () {
var sum = incomingObjects.reduce(function (sum, o) {
return o[6];
}, 0);
// create inc is ruled with probabilities
if (
nextCreate < t &&
Math.random() <
0.01 * dt * // continous time probability
Math.exp(-sum * // more there is object, more it is rare to create new ones
(1 + 5 * Math.exp(-(player-1)/3) - 0.2 * Math.exp(-Math.abs(player-20)/20)) // first rounds have less items
) *
(1 - Math.exp(-playingSince / 5000))
) {
nextCreate = t + 1000 * (1 + Math.random());
return createInc();
}
}
var rotatingLetters = [];
for (var rotatingLettersI=0; rotatingLettersI<26; rotatingLettersI++)
rotatingLetters.push(65+rotatingLettersI);
rotatingLetters.sort(function () {
return Math.random()-0.5;
});
function createInc () {
if (!rotatingLetters.length) return 0;
var pos = Math.random() * borderLength;
var key = rotatingLetters.shift();
for (var i=0; i<incomingObjects.length; ++i) {
var o = incomingObjects[i];
var p = o[0] % borderLength;
if (pos - 60 < p && p < pos + 60) return 0;
}
/*
PARAMS to vary with game difficulty
- higher rotation amplitude
- lower rotation valid amp ratio
- higher rotation speed
*/
var diffMax = 1-Math.exp(-player/5);
var diffMin = 1-Math.exp((1-player)/20);
if (Math.random() > diffMax) diffMin *= Math.random();
var pRotAmp = diffMin + Math.random() * (diffMax-diffMin);
var pRotAmpRatio = diffMin + Math.random() * (diffMax-diffMin);
var pRotSpeed = diffMin + Math.random() * (diffMax-diffMin);
var lvl = Math.floor(2 + 3 * Math.random() * Math.random() + 4 * Math.random() * Math.random() * Math.random());
var ampRot = player<2 ? 0 : Math.PI * (0.8 * Math.random() + 0.05 * lvl) * pRotAmp;
if (ampRot < 0.2) ampRot = 0;
var ampRotRatio =
player > 2 &&
ampRot > Math.exp(-player/4) &&
Math.random() > 0.5 + 0.4 * ((player-3)%8)/8 - 0.5 * (1 - Math.exp(-player/10)) ?
0.9 - 0.5 * pRotAmpRatio - 0.2 * pRotAmp :
1;
if (player == 2) {
ampRot = 0.2 + Math.random();
}
if (player == 3) {
ampRot = 0.2 + Math.random();
ampRotRatio = 0.5 + 0.4 * Math.random();
}
incomingObjects.push([
pos,
// velocity
0.1 + 0.002 * player,
// initial angle
2*Math.PI*Math.random(),
// initial force
10 + 40*Math.random(),
// rot velocity
0.002 + 0.001 * (Math.random() + 0.5 * lvl * Math.random() + Math.random() * player / 30) * pRotSpeed - 0.001 * pRotAmp,
// shape
randomAsteroidShape(lvl),
// level
lvl,
// key
key,
// amplitude rotation
ampRot,
// amplitude rotation valid ratio
ampRotRatio,
// explode time
0
]);
return 1;
}
function applyIncLogic (o) {
if (!o[10]) {
o[0] += o[1] * dt;
o[2] += o[4] * dt;
o[3] = o[3] < 10 ? 60 : o[3] - 0.02 * dt;
}
}
// RENDERING
function drawInc (o) {
var rotC = incRotationCenter(o);
var phase = Math.cos(o[2]);
var rot = phase * o[8] + rotC;
var w = 10 * o[6];
var valid = Math.abs(phase) < o[9];
if (playingSince>0 && lifes && !dying && !o[10]) {
ctx.lineWidth = 1+o[3]/60;
ctx.strokeStyle = valid ? "#7cf" : "#f66";
if (o[8] > 0.1) {
ctx.save();
ctx.rotate(rotC);
ctx.strokeStyle = "#f66";
ctx.beginPath();
ctx.arc(0, 0, w+10, -o[8], -o[8]*o[9]);
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, w+10, o[8]*o[9], o[8]);
ctx.stroke();
ctx.strokeStyle = "#7cf";
ctx.beginPath();
ctx.arc(0, 0, w+10, -o[8] * o[9], o[8] * o[9]);
ctx.stroke();
path([
[w+8, 0],
[w+12, 0]
]);
ctx.stroke();
ctx.restore();
}
ctx.save();
ctx.rotate(rot);
ctx.save();
var mx = 60 + w;
var x = o[3] + w;
ctx.globalAlpha = 0.2;
path([
[0,0],
[mx,0]
]);
ctx.stroke();
ctx.restore();
path([
[0,0],
[x,0]
]);
ctx.stroke();
var r = 6;
path([
[ mx - r, r ],
[ mx, 0],
[ mx - r, -r ]
], 1);
ctx.stroke();
ctx.restore();
}
else {
ctx.strokeStyle = o[10] ? "#f66" : "#999";
}
ctx.save();
path(o[5]);
ctx.fillStyle = "#000";
ctx.fill();
ctx.stroke();
ctx.restore();
var sum = [0, 0];
o[5].forEach(function (p) {
sum[0] += p[0];
sum[1] += p[1];
});
if (!MOBILE && playingSince>0) {
if (helpVisible()) {
ctx.strokeStyle = "#f7c";
}
ctx.translate(sum[0]/o[5].length+1, sum[1]/o[5].length-5);
font(String.fromCharCode(o[7]), 1);
}
}
function drawIncHelp () {
if (!helpVisible()) return;
ctx.strokeStyle = "#f7c";
ctx.lineWidth = 4;
incomingObjects.forEach(function (o) {
var p = incPosition(o);
ctx.beginPath();
ctx.arc(p[0], p[1], 80 + 40 * Math.cos(0.005 * t), 0, 2*Math.PI);
ctx.stroke();
});
}

View File

@ -0,0 +1,41 @@
/* global dt W H */
function euclidPhysics (obj) {
obj[0] += obj[2] * dt;
obj[1] += obj[3] * dt;
}
function polarPhysics (obj) {
var x = Math.cos(obj[2]);
var y = Math.sin(obj[2]);
var s = dt * obj[3];
obj[0] += s * x;
obj[1] += s * y;
}
function destroyOutOfBox (obj, i, arr) {
if (obj[0] < -100 || obj[1] < -100 || obj[0] > W+100 || obj[1] > H+100) {
arr.splice(i, 1);
}
}
function applyLife (obj, i, arr) {
if ((obj[4] -= dt) < 0) {
arr.splice(i, 1);
}
}
function loopOutOfBox (obj) {
if (obj[0] < 0) {
obj[0] += W;
}
else if (obj[0] > W) {
obj[0] -= W;
}
if (obj[1] < 0) {
obj[1] += H;
}
else if (obj[1] > H) {
obj[1] -= H;
}
}

View File

@ -0,0 +1,27 @@
/* global
ctx bullets
*/
function shoot (obj, vel, ang) {
var ax = Math.cos(ang);
var ay = Math.sin(ang);
bullets.push([
obj[0] + 14 * ax,
obj[1] + 14 * ay,
obj[2] + vel * ax,
obj[3] + vel * ay,
1000,
0
]);
}
// RENDERING
function drawBullet () {
ctx.globalAlpha = 1 - Math.random()*Math.random();
ctx.fillStyle = "#00f";
ctx.beginPath();
ctx.arc(0, 0, 2+2.5*Math.random(), 0, 2*Math.PI);
ctx.fill();
}

View File

@ -0,0 +1,139 @@
/* global
g
gl
textureGame
smoothstep
glSetTexture
glBindFBO
glGetFBOTexture
glUniformLocation
glBindTexture
laserFbo
playerFbo
glareFbo
fbo1 fbo2
persistenceFbo
copyShader
glBindShader
laserShader
playerShader
blur1dShader
gameShader
glareShader
persistenceShader
t
excitementSmoothed
gameOver
player
playingSince
lifes
lastLoseShot
shaking
jumping
dying
*/
function drawPostProcessing () {
glSetTexture(textureGame, g);
// Laser
glBindFBO(laserFbo);
glBindShader(laserShader);
gl.uniform1i(glUniformLocation(laserShader, "t"), glBindTexture(textureGame, 0));
gl.drawArrays(gl.TRIANGLES, 0, 6);
// Player / env
glBindFBO(playerFbo);
glBindShader(playerShader);
gl.uniform1f(glUniformLocation(playerShader, "pt"), playingSince / 1000);
gl.uniform1f(glUniformLocation(playerShader, "pl"), player);
gl.uniform1f(glUniformLocation(playerShader, "ex"), gameOver || excitementSmoothed);
gl.uniform1f(glUniformLocation(playerShader, "J"), jumping);
gl.uniform1f(glUniformLocation(playerShader, "P"), playingSince<0 || gameOver || dying ? 0 : 1);
gl.drawArrays(gl.TRIANGLES, 0, 6);
glBindFBO(fbo1);
glBindShader(blur1dShader);
gl.uniform1i(glUniformLocation(blur1dShader, "t"), glBindTexture(glGetFBOTexture(playerFbo), 0));
gl.uniform2f(glUniformLocation(blur1dShader, "dir"), 2, 2 );
gl.drawArrays(gl.TRIANGLES, 0, 6);
glBindFBO(fbo2);
glBindShader(blur1dShader);
gl.uniform1i(glUniformLocation(blur1dShader, "t"), glBindTexture(glGetFBOTexture(fbo1), 0));
gl.uniform2f(glUniformLocation(blur1dShader, "dir"), -2, 2 );
gl.drawArrays(gl.TRIANGLES, 0, 6);
glBindFBO(fbo1);
glBindShader(blur1dShader);
gl.uniform1i(glUniformLocation(blur1dShader, "t"), glBindTexture(glGetFBOTexture(fbo2), 0));
gl.uniform2f(glUniformLocation(blur1dShader, "dir"), 6, 0 );
gl.drawArrays(gl.TRIANGLES, 0, 6);
glBindFBO(playerFbo);
glBindShader(blur1dShader);
gl.uniform1i(glUniformLocation(blur1dShader, "t"), glBindTexture(glGetFBOTexture(fbo1), 0));
gl.uniform2f(glUniformLocation(blur1dShader, "dir"), 0, 2 );
gl.drawArrays(gl.TRIANGLES, 0, 6);
// Glare
glBindFBO(glareFbo);
glBindShader(glareShader);
gl.uniform1i(glUniformLocation(glareShader, "t"), glBindTexture(glGetFBOTexture(laserFbo), 0));
gl.drawArrays(gl.TRIANGLES, 0, 6);
glBindFBO(fbo1);
glBindShader(blur1dShader);
gl.uniform1i(glUniformLocation(blur1dShader, "t"), glBindTexture(glGetFBOTexture(glareFbo), 0));
gl.uniform2f(glUniformLocation(blur1dShader, "dir"), 2, -4 );
gl.drawArrays(gl.TRIANGLES, 0, 6);
glBindFBO(glareFbo);
glBindShader(blur1dShader);
gl.uniform1i(glUniformLocation(blur1dShader, "t"), glBindTexture(glGetFBOTexture(fbo1), 0));
gl.uniform2f(glUniformLocation(blur1dShader, "dir"), 4, -8 );
gl.drawArrays(gl.TRIANGLES, 0, 6);
// Blur
glBindFBO(fbo1);
glBindShader(blur1dShader);
gl.uniform1i(glUniformLocation(blur1dShader, "t"), glBindTexture(glGetFBOTexture(laserFbo), 0));
gl.uniform2f(glUniformLocation(blur1dShader, "dir"), 0.5, 0.5 );
gl.drawArrays(gl.TRIANGLES, 0, 6);
glBindFBO(fbo2);
glBindShader(blur1dShader);
gl.uniform1i(glUniformLocation(blur1dShader, "t"), glBindTexture(glGetFBOTexture(fbo1), 0));
gl.uniform2f(glUniformLocation(blur1dShader, "dir"), -0.5, 0.5 );
gl.drawArrays(gl.TRIANGLES, 0, 6);
glBindFBO(fbo1);
glBindShader(blur1dShader);
gl.uniform1i(glUniformLocation(blur1dShader, "t"), glBindTexture(glGetFBOTexture(fbo2), 0));
gl.uniform2f(glUniformLocation(blur1dShader, "dir"), 1, 0 );
gl.drawArrays(gl.TRIANGLES, 0, 6);
glBindFBO(fbo2);
glBindShader(blur1dShader);
gl.uniform1i(glUniformLocation(blur1dShader, "t"), glBindTexture(glGetFBOTexture(fbo1), 0));
gl.uniform2f(glUniformLocation(blur1dShader, "dir"), 0, 1 );
gl.drawArrays(gl.TRIANGLES, 0, 6);
// Persistence
glBindFBO(fbo1);
glBindShader(persistenceShader);
gl.uniform1i(glUniformLocation(persistenceShader, "t"), glBindTexture(glGetFBOTexture(fbo2), 0));
gl.uniform1i(glUniformLocation(persistenceShader, "r"), glBindTexture(glGetFBOTexture(persistenceFbo), 1));
gl.drawArrays(gl.TRIANGLES, 0, 6);
glBindFBO(persistenceFbo);
glBindShader(copyShader);
gl.uniform1i(glUniformLocation(copyShader, "t"), glBindTexture(glGetFBOTexture(fbo1), 0));
gl.drawArrays(gl.TRIANGLES, 0, 6);
// Final draw
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
glBindShader(gameShader);
gl.uniform1i(glUniformLocation(gameShader, "G"), glBindTexture(glGetFBOTexture(laserFbo), 0));
gl.uniform1i(glUniformLocation(gameShader, "R"), glBindTexture(glGetFBOTexture(persistenceFbo), 1));
gl.uniform1i(glUniformLocation(gameShader, "B"), glBindTexture(glGetFBOTexture(fbo2), 2));
gl.uniform1i(glUniformLocation(gameShader, "L"), glBindTexture(glGetFBOTexture(glareFbo), 3));
gl.uniform1i(glUniformLocation(gameShader, "E"), glBindTexture(glGetFBOTexture(playerFbo), 4));
gl.uniform1f(glUniformLocation(gameShader, "s"),
!player ? smoothstep(-4000, -3000, playingSince) : 1);
gl.uniform1f(glUniformLocation(gameShader, "F"),
smoothstep(300, 0, t-lastLoseShot) +
!gameOver && lifes>4 ? 0.5 * smoothstep(-1, 1, Math.cos(0.01*t)) : 0);
gl.uniform2f(glUniformLocation(gameShader, "k"), shaking[0], shaking[1]);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}

View File

@ -0,0 +1,2 @@
var DEBUG = true; // eslint-disable-line no-unused-vars
var MOBILE = "ontouchstart" in document; // eslint-disable-line no-unused-vars

View File

@ -0,0 +1,2 @@
var DEBUG = false; // eslint-disable-line no-unused-vars
var MOBILE = "ontouchstart" in document; // eslint-disable-line no-unused-vars

View File

@ -0,0 +1,411 @@
/* eslint-disable no-undef */
/* eslint-enable no-unused-vars */
randomAsteroids();
raf=requestAnimationFrame(render);
if (DEBUG) {
/* eslint-disable */
/*
// DEBUG the game over screen
setTimeout(function () {
playingSince = -1;
awaitingContinue = 0;
player = 42;
achievements = [123, 45, 6];
gameOver = 1;
}, 1000);
*/
// Debug the levels
addEventListener("resize", function () {
playingSince = -1;
awaitingContinue = 0;
player ++;
incomingObjects = [];
console.log("player=", player);
/*
ufos.push([
0, 0, 0, 0, 0
]);
*/
});
/*
setTimeout(function () {
killSmoothed ++;
}, 100);
setTimeout(function () {
killSmoothed ++;
}, 2000);
*/
// Debug the incomingObjects
/*
setInterval(function () {
createInc();
if (incomingObjects[0]) sendAsteroid(incomingObjects[0]);
incomingObjects.splice(0, 1);
}, 800);
*/
/* eslint-enable */
}
window._behindAsteroids_send = function () {
createInc();
if (incomingObjects[0]) sendAsteroid(incomingObjects[0]);
incomingObjects.splice(0, 1);
};
// Game Render Loop
var _lastT, _lastCheckSize = -9999;
function render (_t) {
raf=requestAnimationFrame(render);
if (!_lastT) _lastT = _t;
dt = Math.min(100, _t-_lastT);
_lastT = _t;
if (t-_lastCheckSize>200) checkSize();
t += dt; // accumulate the game time (that is not the same as _t)
// UPDATE game
update();
// RENDER game
// Game rendering
ctx = gameCtx;
ctx.save();
drawGame();
ctx.restore();
RENDER_CB();
}
// Game Update Loop
function update () {
playingSince += dt;
if (t-ufoMusicTime>1200) {
ufoMusicTime = t;
if (ufos[0])
play(Aufo);
}
if(!gameOver && !awaitingContinue) {
if (playingSince > 0 && !achievements) {
achievements = [0,0,0];
}
var i;
var nbSpaceshipBullets = 0;
if (!dying && playingSince>0 && t-musicPaused>5000 && player > 2 && !ufos.length) {
combosTarget = Math.floor(30 - 25 * Math.exp(-(player-3)/15));
var musicFreq = 3*combos/combosTarget;
if (combos > combosTarget) {
musicPaused = t;
neverUFOs = combos = 0;
ufos.push([
W * Math.random(),
H * Math.random(),
0,
0,
0
]);
achievements[2] ++;
}
musicPhase += musicFreq*2*Math.PI*dt/1000;
if ((Math.sin(musicPhase) > 0) !== musicTick) {
musicTick = !musicTick;
play(musicTick ? Amusic1 : Amusic2);
}
}
// randomly send some asteroids
/*
if (Math.random() < 0.001 * dt)
randomInGameAsteroid();
*/
// player lifecycle
if (lifes == 0 && playingSince > 0) {
// player enter
resurrectionTime = t;
lifes = 4;
player++;
score = 0;
scoreForLife = 10000;
jumpingAmp = 0;
jumpingFreq = 0;
asteroids = [];
ufos = [];
play(Acoin);
if (player > 1) {
//localStorage.ba_pl = player;
//localStorage.ba_ach = achievements;
}
}
// inc lifecycle
if (playingSince > 1000 && !dying) {
for (i = 0; i < incomingObjects.length; i++) {
var o = incomingObjects[i];
if (!o[10]) {
var p = incPosition(o);
var matchingTap = tap && circleCollides(tap, p, (MOBILE ? 60 : 20) + 10 * o[6]);
if (keys[o[7]] || matchingTap) {
// send an asteroid
neverPlayed = tap = keys[o[7]] = 0;
if (sendAsteroid(o)) {
achievements[0] ++;
if (player > 3) combos ++;
incomingObjects.splice(i--, 1);
}
else {
// failed to aim (red aiming)
score += 5000;
combos = 0;
lastLoseShot = o[10] = t;
}
}
}
else {
if (t-o[10] > 1000)
incomingObjects.splice(i--, 1);
}
}
tap = 0;
while(maybeCreateInc());
}
// spaceship lifecycle
if (dying && t-dying > 2000 + (lifes>1 ? 0 : 2000)) {
dying = 0;
spaceship = [ W/2, H/2, 0, 0, 0 ];
if (--lifes) {
resurrectionTime = t;
}
else {
// Player lost. game over
playingSince = -5000;
randomAsteroids();
ufos = [];
setTimeout(function(){ play(Aleave); }, 1000);
}
}
// score lifecycle
if (score >= scoreForLife) {
lastExtraLife = t;
lifes ++;
scoreForLife += 10000;
play(Alife);
if (lifes > 5) {
gameOver = 1;
incomingObjects = [];
ufos = [];
randomAsteroids();
//localStorage.ba_pl=0;
}
}
if (!dying && playingSince>0 && t - lastScoreIncrement > 100) {
score += 10;
lastScoreIncrement = t;
}
best = Math.max(best, score);
// collision
bullets.forEach(function (bull, i) {
if (!bull[5]) nbSpaceshipBullets ++;
var j;
if (bull[4]<900) {
// bullet-spaceship collision
if (!dying && circleCollides(bull, spaceship, 20)) {
explose(bull);
bullets.splice(i, 1);
spaceshipDie();
return;
}
// bullet-ufo collision
for (j = 0; j < ufos.length; ++j) {
var ufo = ufos[j];
if (circleCollides(bull, ufo, 20)) {
explose(bull);
bullets.splice(i, 1);
ufos.splice(j, 1);
return;
}
}
}
for (j = 0; j < asteroids.length; ++j) {
var aster = asteroids[j];
var lvl = aster[5];
// bullet-asteroid collision
if (circleCollides(bull, aster, 10 * lvl)) {
explose(bull);
bullets.splice(i, 1);
explodeAsteroid(j);
score += 50 * Math.floor(0.4 * (6 - lvl) * (6 - lvl));
return;
}
}
});
if (!dying && playingSince > 0) asteroids.forEach(function (aster, j) {
// asteroid-spaceship collision
if (circleCollides(aster, spaceship, 10 + 10 * aster[5])) {
if (t - resurrectionTime < 200) {
// if spaceship just resurect, will explode the asteroid
explodeAsteroid(j);
}
else {
// otherwise, player die
explose(spaceship);
spaceshipDie();
}
}
});
// run spaceship AI
AIexcitement = 0;
if (!dying && playingSince > 0) {
var ax = Math.cos(spaceship[4]);
var ay = Math.sin(spaceship[4]);
// ai logic (determine the 3 inputs)
aiLogic(1-Math.exp(-(player-0.8)/14));
// apply ai inputs with game logic
var rotSpeed = 0.004 + 0.003 * (1-Math.exp(-player/40));
var accSpeed = 0.0003 - 0.0002 * Math.exp(-(player-1)/5) + 0.00001 * player;
var shotRate = 100 + 1000 * Math.exp(-(player-1)/8) + 300 * Math.exp(-player/20);
spaceship[2] += AIboost * dt * accSpeed * ax;
spaceship[3] += AIboost * dt * accSpeed * ay;
spaceship[4] = normAngle(spaceship[4] + AIrotate * dt * rotSpeed);
if (nbSpaceshipBullets < 3) {
if (AIshoot && t-lastBulletShoot > shotRate) {
lastBulletShoot = t;
play(Ashot);
shoot(spaceship, 0.3, spaceship[4]);
}
}
}
}
euclidPhysics(spaceship);
asteroids.forEach(polarPhysics);
ufos.forEach(euclidPhysics);
bullets.forEach(euclidPhysics);
particles.forEach(polarPhysics);
ufos.forEach(applyUFOlogic);
incomingObjects.forEach(applyIncLogic);
particles.forEach(applyLife);
loopOutOfBox(spaceship);
asteroids.forEach(playingSince > 0 && !awaitingContinue && !gameOver ? destroyOutOfBox : loopOutOfBox);
ufos.forEach(loopOutOfBox);
bullets.forEach(applyLife);
bullets.forEach(loopOutOfBox);
excitementSmoothed += 0.04 * (AIexcitement - excitementSmoothed);
AIboostSmoothed += 0.04 * (AIboost - AIboostSmoothed);
// handling jumping / shaking
killSmoothed -= dt * 0.0003 * killSmoothed;
jumpingAmpSmoothed += 0.04 * (jumpingAmp - jumpingAmpSmoothed);
jumpingFreqSmoothed += 0.04 * (jumpingFreq - jumpingFreqSmoothed);
if (killSmoothed > 1.3) {
if (jumpingAmp < 0.5) {
jumpingFreq = 1 + Math.random();
jumpingAmp ++;
}
}
if (killSmoothed < 0.8) {
jumpingAmp = 0;
}
var prevPhase = jumpingPhase;
jumpingPhase += jumpingFreq *2*Math.PI*dt/1000;
jumping = jumpingAmpSmoothed * Math.pow(Math.cos(jumpingPhase), 2.0);
if (Math.cos(prevPhase) < 0 && 0 < Math.cos(jumpingPhase)) {
jumpingFreq = 1 + 3 * Math.random() * Math.random();
}
if (jumpingAmp < 0.5) {
jumpingAmpSmoothed += 0.04 * (jumpingAmp - jumpingAmpSmoothed);
}
var shake = jumpingAmp * Math.pow(smoothstep(0.2, 0.0, jumping), 0.5);
if (shake > 0.5 && t-lastJump>100) {
play(Ajump);
lastJump = t;
}
shaking = [
30 * shake * (Math.random()-0.5) / FW,
30 * shake * (Math.random()-0.5) / FH
];
}
// Game DRAWING
function drawGame () {
ctx.save();
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, W, H);
ctx.restore();
renderCollection(asteroids, drawAsteroid);
renderCollection(ufos, drawUFO);
renderCollection(bullets, drawBullet);
renderCollection(particles, drawParticle);
if (playingSince > 0 && !awaitingContinue && !gameOver) {
ctx.save();
translateTo(spaceship);
drawSpaceship(spaceship);
ctx.restore();
}
drawGameUI();
drawGlitch();
}
function translateTo (p) {
ctx.translate(p[0], p[1]);
}
function renderCollection (coll, draw) {
for (var i=0; i<coll.length; ++i) {
ctx.save();
translateTo(coll[i]);
draw(coll[i]);
ctx.restore();
}
}

View File

@ -0,0 +1,86 @@
/* global
keys
tap:true
MOBILE
c
d
gameOver
W
gameScale
achievements:true
player:true
playingSince:true
awaitingContinue:true
*/
/*
for (var i=0; i<99; ++i) keys[i] = 0;
var fullScreenRequested = 0;
function onTap (e) {
if (MOBILE && !fullScreenRequested && d.webkitRequestFullScreen){
d.webkitRequestFullScreen();
fullScreenRequested = 1;
}
var r = c.getBoundingClientRect(),
x = (e.clientX - r.left) / gameScale,
y = (e.clientY - r.top) / gameScale;
if (gameOver) {
if(280 < y && y < 400) {
if (W/2 - 180 < x && x < W/2 - 20) {
open("https://twitter.com/intent/tweet?via=greweb&url="+
encodeURIComponent(location.href)+
"&text="+
encodeURIComponent(
"Reached Level "+player+
" ("+(player*25)+"¢) with "+
achievements[0]+"⬠ "+
achievements[1]+"ᐃ "+
achievements[2]+"🝞"
));
}
else if (W/2 + 20 < x && x < W/2 + 180) {
location.reload();
}
}
}
else if (awaitingContinue) {
if (playingSince>0 && 170<y && y<310) {
// continue game action
if (x<W/2) { // YES
player = awaitingContinue-1;
playingSince = awaitingContinue = 0;
achievements = localStorage.ba_ach.split(",").map(function (v) {
return parseInt(v, 10);
});
}
else { // NO
playingSince = awaitingContinue = 0;
}
}
}
else {
tap = [x, y];
}
}
if (MOBILE) {
addEventListener("touchstart", function (e) {
e.preventDefault();
onTap(e.changedTouches[0]);
});
}
else {
addEventListener("click", function (e) {
e.preventDefault();
onTap(e);
});
addEventListener("keydown", function (e) {
keys[e.which] = 1;
});
addEventListener("keyup", function (e) {
keys[e.which] = 0;
});
}
*/

View File

@ -0,0 +1,353 @@
/* global ctx, path */
// implementing by hand Asteroid's font. see http://www.dafont.com/hyperspace.font
var FONT0 = [ // 0
[0, 0],
[2, 0],
[2, 2],
[0, 2],
[0, 0]
];
var FONT5 = [ // 5
[2, 0],
[0, 0],
[0, 1],
[2, 1],
[2, 2],
[0, 2]
];
var FONT = [
FONT0,
[ // 1
[1, 0],
[1, 2]
],
[ // 2
[0, 0],
[2, 0],
[2, 1],
[0, 1],
[0, 2],
[2, 2]
],
[ // 3
[0, 0],
[2, 0],
[2, 2],
[0, 2],
,
[0, 1],
[2, 1]
],
[ // 4
[0, 0],
[0, 1],
[2, 1],
,
[2, 0],
[2, 2]
],
FONT5,
[ // 6
[0, 0],
[0, 2],
[2, 2],
[2, 1],
[0, 1]
],
[ // 7
[0, 0],
[2, 0],
[2, 2]
],
[ // 8
[0, 0],
[2, 0],
[2, 2],
[0, 2],
[0, 0],
,
[0, 1],
[2, 1]
],
[ // 9
[2, 2],
[2, 0],
[0, 0],
[0, 1],
[2, 1]
]
];
[
[// A
[0,2],
[0,2/3],
[1,0],
[2,2/3],
[2,2],
,
[0,4/3],
[2,4/3]
],
[ // B
[0, 1],
[0, 0],
[4/3,0],
[2,1/3],
[2,2/3],
[4/3,1],
[0,1],
[0,2],
[4/3,2],
[2,5/3],
[2,4/3],
[4/3,1]
],
[// C
[2,0],
[0,0],
[0,2],
[2,2]
],
[// D
[0,0],
[1,0],
[2,2/3],
[2,4/3],
[1,2],
[0,2],
[0,0]
],
[// E
[2,0],
[0,0],
[0,2],
[2,2],
,
[0,1],
[1.5,1]
],
[// F
[2,0],
[0,0],
[0,2],
,
[0,1],
[2,1]
],
[// G
[2,2/3],
[2,0],
[0,0],
[0,2],
[2,2],
[2,4/3],
[1,4/3]
],
[// H
[0,0],
[0,2],
,
[2,0],
[2,2],
,
[0,1],
[2,1]
],
[// I
[0,0],
[2,0],
,
[1,0],
[1,2],
,
[0,2],
[2,2]
],
[// J
[2,0],
[2,2],
[1,2],
[0,4/3]
],
[// K
[0,0],
[0,2],
,
[2,0],
[0,1],
[2,2]
],
[// L
[0,0],
[0,2],
[2,2]
],
[// M
[0,2],
[0,0],
[1,2/3],
[2,0],
[2,2]
],
[// N
[0,2],
[0,0],
[2,2],
[2,0]
],
FONT0,// O
[// P
[0,2],
[0,0],
[2,0],
[2,1],
[0,1]
],
[// Q
[0,0],
[2,0],
[2,4/3],
[1,2],
[0,2],
[0,0],
,
[2,2],
[1,4/3]
],
[// R
[0,2],
[0,0],
[2,0],
[2,1],
[0,1],
[2,2]
],
FONT5,// S
[// T
[0,0],
[2,0],
,
[1,0],
[1,2]
],
[// U
[0,0],
[0,2],
[2,2],
[2,0]
],
[// V
[0,0],
[1,2],
[2,0]
],
[// W
[0,0],
[0,2],
[1,4/3],
[2,2],
[2,0]
],
[// X
[0,0],
[2,2],
,
[2,0],
[0,2]
],
[// Y
[0,0],
[1,2/3],
[2,0],
,
[1,2/3],
[1,2]
],
[// Z
[0,0],
[2,0],
[0,2],
[2,2]
]
].forEach(function (c, i) {
FONT[String.fromCharCode(65+i)] = c;
});
var dot = FONT["."] = [
[1, 1.8],
[1, 2]
];
FONT[":"] = [
[1, 0],
[1, 0.2],
,
[1, 1.8],
[1, 2]
];
FONT["'"] = [
[1, 0],
[1, 2/3]
];
FONT["ᐃ"] = [
[ 1, 0 ],
[ 1.8, 2 ],
[ 1, 1.6 ],
[ 0.2, 2 ],
[ 1, 0 ]
/*
[-4, -4],
[ 10, 0],
[ -4, 4],
[ -3, 0]
*/
];
FONT["!"] = [
[1, 0],
[1, 1.5],
,
].concat(dot);
FONT["?"] = [
[0, 0],
[2, 0],
[2, 1],
[1, 1],
[1, 1.5],
,
].concat(dot);
FONT["x"] = [
[0,1],
[2,2],
,
[2,1],
[0,2]
];
FONT["¢"] = [
[1,0],
[1,2],
,
[1.5,0.5],
[0.5,0.5],
[0.5,1.5],
[1.5,1.5]
];
// oO ASTEROIDS font with fontSize and align (-1:right, 0:center, 1:left)
// will side effect some ctx.translate() (that you could benefit to make text follow)
function font (txt, fontSize, align) { // eslint-disable-line
var l = fontSize*11*txt.length;
ctx.translate(align ? (align>0 ? 0 : -l) : -l/2, 0);
for (var i=0; i<txt.length; i++) {
path(FONT[txt[i]] && FONT[txt[i]].map(function (o) {
return o && [4*fontSize*o[0], 5*fontSize*o[1]];
}), 1);
ctx.lineJoin = "round";
ctx.stroke();
ctx.translate(fontSize*11, 0);
}
}

View File

@ -0,0 +1,35 @@
/* global jsfxr */
var audioCtx, audioDest, audio, play; // eslint-disable-line
var AudioContext = window.AudioContext || window.webkitAudioContext;
if (AudioContext) {
audioCtx = new AudioContext();
audioDest = audioCtx.createDynamicsCompressor();
var gain = audioCtx.createGain();
gain.gain.value = 0.1;
audioDest.connect(gain);
gain.connect(audioCtx.destination);
audio = function (conf) { // eslint-disable-line no-unused-vars
var o = [];
jsfxr(conf, audioCtx, function (buf) {
o.push(buf);
});
return o;
};
play = function (o) { // eslint-disable-line no-unused-vars
if (!o[0]) return;
var source = audioCtx.createBufferSource();
source.buffer = o[0];
source.start(0);
source.connect(audioDest);
setTimeout(function () {
source.disconnect(audioDest);
}, o[0].duration * 1000 + 300);
};
}
else {
audio = play = function(){};
}

View File

@ -0,0 +1,490 @@
/**
* SfxrParams
*
* Copyright 2010 Thomas Vian
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @author Thomas Vian
*/
/* eslint-disable */
/** @constructor */
function SfxrParams() {
//--------------------------------------------------------------------------
//
// Settings String Methods
//
//--------------------------------------------------------------------------
/**
* Parses a settings array into the parameters
* @param array Array of the settings values, where elements 0 - 23 are
* a: waveType
* b: attackTime
* c: sustainTime
* d: sustainPunch
* e: decayTime
* f: startFrequency
* g: minFrequency
* h: slide
* i: deltaSlide
* j: vibratoDepth
* k: vibratoSpeed
* l: changeAmount
* m: changeSpeed
* n: squareDuty
* o: dutySweep
* p: repeatSpeed
* q: phaserOffset
* r: phaserSweep
* s: lpFilterCutoff
* t: lpFilterCutoffSweep
* u: lpFilterResonance
* v: hpFilterCutoff
* w: hpFilterCutoffSweep
* x: masterVolume
* @return If the string successfully parsed
*/
this.ss = function(values)
{
for ( var i = 0; i < 24; i++ )
{
this[String.fromCharCode( 97 + i )] = values[i] || 0;
}
// I moved this here from the r(true) function
if (this['c'] < .01) {
this['c'] = .01;
}
var totalTime = this['b'] + this['c'] + this['e'];
if (totalTime < .18) {
var multiplier = .18 / totalTime;
this['b'] *= multiplier;
this['c'] *= multiplier;
this['e'] *= multiplier;
}
}
}
/**
* SfxrSynth
*
* Copyright 2010 Thomas Vian
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @author Thomas Vian
*/
/** @constructor */
function SfxrSynth() {
// All variables are kept alive through function closures
//--------------------------------------------------------------------------
//
// Sound Parameters
//
//--------------------------------------------------------------------------
this._p = new SfxrParams(); // Params instance
//--------------------------------------------------------------------------
//
// Synth Variables
//
//--------------------------------------------------------------------------
var _envelopeLength0, // Length of the attack stage
_envelopeLength1, // Length of the sustain stage
_envelopeLength2, // Length of the decay stage
_period, // Period of the wave
_maxPeriod, // Maximum period before sound stops (from minFrequency)
_slide, // Note slide
_deltaSlide, // Change in slide
_changeAmount, // Amount to change the note by
_changeTime, // Counter for the note change
_changeLimit, // Once the time reaches this limit, the note changes
_squareDuty, // Offset of center switching point in the square wave
_dutySweep; // Amount to change the duty by
//--------------------------------------------------------------------------
//
// Synth Methods
//
//--------------------------------------------------------------------------
/**
* rs the runing variables from the params
* Used once at the start (total r) and for the repeat effect (partial r)
*/
this.r = function() {
// Shorter reference
var p = this._p;
_period = 100 / (p['f'] * p['f'] + .001);
_maxPeriod = 100 / (p['g'] * p['g'] + .001);
_slide = 1 - p['h'] * p['h'] * p['h'] * .01;
_deltaSlide = -p['i'] * p['i'] * p['i'] * .000001;
if (!p['a']) {
_squareDuty = .5 - p['n'] / 2;
_dutySweep = -p['o'] * .00005;
}
_changeAmount = 1 + p['l'] * p['l'] * (p['l'] > 0 ? -.9 : 10);
_changeTime = 0;
_changeLimit = p['m'] == 1 ? 0 : (1 - p['m']) * (1 - p['m']) * 20000 + 32;
}
// I split the r() function into two functions for better readability
this.tr = function() {
this.r();
// Shorter reference
var p = this._p;
// Calculating the length is all that remained here, everything else moved somewhere
_envelopeLength0 = p['b'] * p['b'] * 100000;
_envelopeLength1 = p['c'] * p['c'] * 100000;
_envelopeLength2 = p['e'] * p['e'] * 100000 + 12;
// Full length of the volume envelop (and therefore sound)
// Make sure the length can be divided by 3 so we will not need the padding "==" after base64 encode
return ((_envelopeLength0 + _envelopeLength1 + _envelopeLength2) / 3 | 0) * 3;
}
/**
* Writes the wave to the supplied buffer ByteArray
* @param buffer A ByteArray to write the wave to
* @return If the wave is finished
*/
this.sw = function(buffer, length) {
// Shorter reference
var p = this._p;
// If the filters are active
var _filters = p['s'] != 1 || p['v'],
// Cutoff multiplier which adjusts the amount the wave position can move
_hpFilterCutoff = p['v'] * p['v'] * .1,
// Speed of the high-pass cutoff multiplier
_hpFilterDeltaCutoff = 1 + p['w'] * .0003,
// Cutoff multiplier which adjusts the amount the wave position can move
_lpFilterCutoff = p['s'] * p['s'] * p['s'] * .1,
// Speed of the low-pass cutoff multiplier
_lpFilterDeltaCutoff = 1 + p['t'] * .0001,
// If the low pass filter is active
_lpFilterOn = p['s'] != 1,
// masterVolume * masterVolume (for quick calculations)
_masterVolume = p['x'] * p['x'],
// Minimum frequency before stopping
_minFreqency = p['g'],
// If the phaser is active
_phaser = p['q'] || p['r'],
// Change in phase offset
_phaserDeltaOffset = p['r'] * p['r'] * p['r'] * .2,
// Phase offset for phaser effect
_phaserOffset = p['q'] * p['q'] * (p['q'] < 0 ? -1020 : 1020),
// Once the time reaches this limit, some of the iables are r
_repeatLimit = p['p'] ? ((1 - p['p']) * (1 - p['p']) * 20000 | 0) + 32 : 0,
// The punch factor (louder at begining of sustain)
_sustainPunch = p['d'],
// Amount to change the period of the wave by at the peak of the vibrato wave
_vibratoAmplitude = p['j'] / 2,
// Speed at which the vibrato phase moves
_vibratoSpeed = p['k'] * p['k'] * .01,
// The type of wave to generate
_waveType = p['a'];
var _envelopeLength = _envelopeLength0, // Length of the current envelope stage
_envelopeOverLength0 = 1 / _envelopeLength0, // (for quick calculations)
_envelopeOverLength1 = 1 / _envelopeLength1, // (for quick calculations)
_envelopeOverLength2 = 1 / _envelopeLength2; // (for quick calculations)
// Damping muliplier which restricts how fast the wave position can move
var _lpFilterDamping = 5 / (1 + p['u'] * p['u'] * 20) * (.01 + _lpFilterCutoff);
if (_lpFilterDamping > .8) {
_lpFilterDamping = .8;
}
_lpFilterDamping = 1 - _lpFilterDamping;
var _finished = false, // If the sound has finished
_envelopeStage = 0, // Current stage of the envelope (attack, sustain, decay, end)
_envelopeTime = 0, // Current time through current enelope stage
_envelopeVolume = 0, // Current volume of the envelope
_hpFilterPos = 0, // Adjusted wave position after high-pass filter
_lpFilterDeltaPos = 0, // Change in low-pass wave position, as allowed by the cutoff and damping
_lpFilterOldPos, // Previous low-pass wave position
_lpFilterPos = 0, // Adjusted wave position after low-pass filter
_periodTemp, // Period modified by vibrato
_phase = 0, // Phase through the wave
_phaserInt, // Integer phaser offset, for bit maths
_phaserPos = 0, // Position through the phaser buffer
_pos, // Phase expresed as a Number from 0-1, used for fast sin approx
_repeatTime = 0, // Counter for the repeats
_sample, // Sub-sample calculated 8 times per actual sample, averaged out to get the super sample
_superSample, // Actual sample writen to the wave
_vibratoPhase = 0; // Phase through the vibrato sine wave
// Buffer of wave values used to create the out of phase second wave
var _phaserBuffer = new Array(1024),
// Buffer of random values used to generate noise
_noiseBuffer = new Array(32);
for (var i = _phaserBuffer.length; i--; ) {
_phaserBuffer[i] = 0;
}
for (var i = _noiseBuffer.length; i--; ) {
_noiseBuffer[i] = Math.random() * 2 - 1;
}
for (var i = 0; i < length; i++) {
if (_finished) {
return i;
}
// Repeats every _repeatLimit times, partially rting the sound parameters
if (_repeatLimit) {
if (++_repeatTime >= _repeatLimit) {
_repeatTime = 0;
this.r();
}
}
// If _changeLimit is reached, shifts the pitch
if (_changeLimit) {
if (++_changeTime >= _changeLimit) {
_changeLimit = 0;
_period *= _changeAmount;
}
}
// Acccelerate and apply slide
_slide += _deltaSlide;
_period *= _slide;
// Checks for frequency getting too low, and stops the sound if a minFrequency was set
if (_period > _maxPeriod) {
_period = _maxPeriod;
if (_minFreqency > 0) {
_finished = true;
}
}
_periodTemp = _period;
// Applies the vibrato effect
if (_vibratoAmplitude > 0) {
_vibratoPhase += _vibratoSpeed;
_periodTemp *= 1 + Math.sin(_vibratoPhase) * _vibratoAmplitude;
}
_periodTemp |= 0;
if (_periodTemp < 8) {
_periodTemp = 8;
}
// Sweeps the square duty
if (!_waveType) {
_squareDuty += _dutySweep;
if (_squareDuty < 0) {
_squareDuty = 0;
} else if (_squareDuty > .5) {
_squareDuty = .5;
}
}
// Moves through the different stages of the volume envelope
if (++_envelopeTime > _envelopeLength) {
_envelopeTime = 0;
switch (++_envelopeStage) {
case 1:
_envelopeLength = _envelopeLength1;
break;
case 2:
_envelopeLength = _envelopeLength2;
}
}
// Sets the volume based on the position in the envelope
switch (_envelopeStage) {
case 0:
_envelopeVolume = _envelopeTime * _envelopeOverLength0;
break;
case 1:
_envelopeVolume = 1 + (1 - _envelopeTime * _envelopeOverLength1) * 2 * _sustainPunch;
break;
case 2:
_envelopeVolume = 1 - _envelopeTime * _envelopeOverLength2;
break;
case 3:
_envelopeVolume = 0;
_finished = true;
}
// Moves the phaser offset
if (_phaser) {
_phaserOffset += _phaserDeltaOffset;
_phaserInt = _phaserOffset | 0;
if (_phaserInt < 0) {
_phaserInt = -_phaserInt;
} else if (_phaserInt > 1023) {
_phaserInt = 1023;
}
}
// Moves the high-pass filter cutoff
if (_filters && _hpFilterDeltaCutoff) {
_hpFilterCutoff *= _hpFilterDeltaCutoff;
if (_hpFilterCutoff < .00001) {
_hpFilterCutoff = .00001;
} else if (_hpFilterCutoff > .1) {
_hpFilterCutoff = .1;
}
}
_superSample = 0;
for (var j = 8; j--; ) {
// Cycles through the period
_phase++;
if (_phase >= _periodTemp) {
_phase %= _periodTemp;
// Generates new random noise for this period
if (_waveType == 3) {
for (var n = _noiseBuffer.length; n--; ) {
_noiseBuffer[n] = Math.random() * 2 - 1;
}
}
}
// Gets the sample from the oscillator
switch (_waveType) {
case 0: // Square wave
_sample = ((_phase / _periodTemp) < _squareDuty) ? .5 : -.5;
break;
case 1: // Saw wave
_sample = 1 - _phase / _periodTemp * 2;
break;
case 2: // Sine wave (fast and accurate approx)
_pos = _phase / _periodTemp;
_pos = (_pos > .5 ? _pos - 1 : _pos) * 6.28318531;
_sample = 1.27323954 * _pos + .405284735 * _pos * _pos * (_pos < 0 ? 1 : -1);
_sample = .225 * ((_sample < 0 ? -1 : 1) * _sample * _sample - _sample) + _sample;
break;
case 3: // Noise
_sample = _noiseBuffer[Math.abs(_phase * 32 / _periodTemp | 0)];
}
// Applies the low and high pass filters
if (_filters) {
_lpFilterOldPos = _lpFilterPos;
_lpFilterCutoff *= _lpFilterDeltaCutoff;
if (_lpFilterCutoff < 0) {
_lpFilterCutoff = 0;
} else if (_lpFilterCutoff > .1) {
_lpFilterCutoff = .1;
}
if (_lpFilterOn) {
_lpFilterDeltaPos += (_sample - _lpFilterPos) * _lpFilterCutoff;
_lpFilterDeltaPos *= _lpFilterDamping;
} else {
_lpFilterPos = _sample;
_lpFilterDeltaPos = 0;
}
_lpFilterPos += _lpFilterDeltaPos;
_hpFilterPos += _lpFilterPos - _lpFilterOldPos;
_hpFilterPos *= 1 - _hpFilterCutoff;
_sample = _hpFilterPos;
}
// Applies the phaser effect
if (_phaser) {
_phaserBuffer[_phaserPos % 1024] = _sample;
_sample += _phaserBuffer[(_phaserPos - _phaserInt + 1024) % 1024];
_phaserPos++;
}
_superSample += _sample;
}
// Averages out the super samples and applies volumes
_superSample *= .125 * _envelopeVolume * _masterVolume;
// Clipping if too loud
buffer[i] = _superSample >= 1 ? 32767 : _superSample <= -1 ? -32768 : _superSample * 32767 | 0;
}
return length;
}
}
// Adapted from http://codebase.es/riffwave/
var synth = new SfxrSynth();
// Export for the Closure Compiler
function jsfxr (settings, audioCtx, cb) {
// Initialize SfxrParams
synth._p.ss(settings);
// Synthesize Wave
var envelopeFullLength = synth.tr();
var data = new Uint8Array(((envelopeFullLength + 1) / 2 | 0) * 4 + 44);
var used = synth.sw(new Uint16Array(data.buffer, 44), envelopeFullLength) * 2;
var dv = new Uint32Array(data.buffer, 0, 44);
// Initialize header
dv[0] = 0x46464952; // "RIFF"
dv[1] = used + 36; // put total size here
dv[2] = 0x45564157; // "WAVE"
dv[3] = 0x20746D66; // "fmt "
dv[4] = 0x00000010; // size of the following
dv[5] = 0x00010001; // Mono: 1 channel, PCM format
dv[6] = 0x0000AC44; // 44,100 samples per second
dv[7] = 0x00015888; // byte rate: two bytes per sample
dv[8] = 0x00100002; // 16 bits per sample, aligned on every two bytes
dv[9] = 0x61746164; // "data"
dv[10] = used; // put number of samples here
// Base64 encoding written by me, @maettig
used += 44;
var i = 0,
base64Characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
output = 'data:audio/wav;base64,';
for (; i < used; i += 3)
{
var a = data[i] << 16 | data[i + 1] << 8 | data[i + 2];
output += base64Characters[a >> 18] + base64Characters[a >> 12 & 63] + base64Characters[a >> 6 & 63] + base64Characters[a & 63];
}
audioCtx && audioCtx.decodeAudioData(data.buffer, cb);
return output;
}

View File

@ -0,0 +1,31 @@
// normalize radian angle between -PI and PI (assuming it is not too far)
function normAngle (a) {
return a < -Math.PI ? a + 2*Math.PI :
a>Math.PI ? a - 2*Math.PI : a;
}
function smoothstep (min, max, value) {
var x = Math.max(0, Math.min(1, (value-min)/(max-min)));
return x*x*(3 - 2*x);
}
function scoreTxt (s) {
return (s<=9?"0":"")+s;
}
function dist (a, b) {
var x = a[0]-b[0];
var y = a[1]-b[1];
return Math.sqrt(x * x + y * y);
}
function length (v) {
return Math.sqrt(v[0]*v[0]+v[1]*v[1]);
}
function circleCollides (a, b, r) {
var x = a[0] - b[0];
var y = a[1] - b[1];
return x*x+y*y < r*r;
}

View File

@ -0,0 +1,16 @@
/* global ctx */
function path (pts, noclose) { // eslint-disable-line no-unused-vars
ctx.beginPath();
var mv = 1;
for (var i = 0; pts && i<pts.length; ++i) {
var p = pts[i];
if (p) {
if (mv) ctx.moveTo(p[0], p[1]);
else ctx.lineTo(p[0], p[1]);
mv = 0;
}
else mv = 1;
}
if (!noclose) ctx.closePath();
}

View File

@ -0,0 +1,80 @@
/* global gl, W, H, DEBUG */
function glCreateShader (vert, frag) {
var handle, type = gl.VERTEX_SHADER, src = vert;
handle = gl.createShader(type);
gl.shaderSource(handle, src);
gl.compileShader(handle);
var vertex = handle;
if (DEBUG) {
if (!gl.getShaderParameter(handle, gl.COMPILE_STATUS))
throw gl.getShaderInfoLog(handle);
}
type = gl.FRAGMENT_SHADER;
src = frag;
handle = gl.createShader(type);
gl.shaderSource(handle, src);
gl.compileShader(handle);
var fragment = handle;
if (DEBUG) {
if (!gl.getShaderParameter(handle, gl.COMPILE_STATUS))
throw gl.getShaderInfoLog(handle);
}
var program = gl.createProgram();
gl.attachShader(program, vertex);
gl.attachShader(program, fragment);
gl.linkProgram(program);
if (DEBUG) {
if (!gl.getProgramParameter(program, gl.LINK_STATUS))
throw gl.getProgramInfoLog(program);
}
gl.useProgram(program);
var p = gl.getAttribLocation(program, "p");
gl.enableVertexAttribArray(p);
gl.vertexAttribPointer(p, 2, gl.FLOAT, false, 0, 0);
return [program];
}
function glBindShader (shader) {
gl.useProgram(shader[0]);
}
function glUniformLocation(shader, name) {
return shader[name] || (shader[name] = gl.getUniformLocation(shader[0], name));
}
function glCreateTexture () {
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
return tex;
}
function glSetTexture (t, value) {
gl.bindTexture(gl.TEXTURE_2D, t);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, value);
}
function glBindTexture (t, unit) {
gl.activeTexture(gl.TEXTURE0 + unit);
gl.bindTexture(gl.TEXTURE_2D, t);
return unit;
}
function glCreateFBO () {
var handle = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, handle);
var color = glCreateTexture();
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, W, H, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, color, 0);
return [handle, color];
}
function glBindFBO (fbo) {
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo[0]);
}
function glGetFBOTexture (fbo) {
return fbo[1];
}

View File

@ -0,0 +1,29 @@
/* global
particles play Aexplosion1 Aexplosion2 ctx
*/
function explose (o) {
play(Math.random()<0.5 ? Aexplosion1 : Aexplosion2);
var n = Math.floor(19 + 9 * Math.random());
for (var i = 0; i < n; ++i) {
var l = 30 * Math.random() - 10;
var a = (Math.random() + 2 * Math.PI * i) / n;
particles.push([
o[0] + l * Math.cos(a),
o[1] + l * Math.sin(a),
a,
0.06,
Math.random()<0.3 ? 0 : 1000
]);
}
}
// RENDERING
function drawParticle () {
ctx.globalAlpha = 0.8;
ctx.fillStyle = "#f00";
ctx.beginPath();
ctx.arc(0, 0, 1, 0, 2*Math.PI);
ctx.fill();
}

View File

@ -0,0 +1,32 @@
return {
dispose: function(){
cancelAnimationFrame(raf);
},
getWebGLParams: function () {
var pt = playingSince / 1000;
var pl = player;
var ex = gameOver || excitementSmoothed;
var J = jumping;
var P = playingSince<0 || gameOver || dying ? 0 : 1;
var s = !player ? smoothstep(-4000, -3000, playingSince) : 1;
var F = smoothstep(300, 0, t-lastLoseShot) +
!gameOver && lifes>4 ? 0.5 * smoothstep(-1, 1, Math.cos(0.01*t)) : 0;
var k = [ shaking[0], shaking[1] ];
return {
pt: pt,
pl: pl,
ex: ex,
J: J,
P: P,
s: s,
F: F,
k: k,
W: W,
H: H,
S: SEED
};
}
};
});

View File

@ -0,0 +1,3 @@
/* eslint-disable */
module.exports = (function(d,g,RENDER_CB){
var raf;

View File

@ -0,0 +1,37 @@
/* global g d MOBILE
gameScale: true
glCreateFBO glCreateShader glCreateTexture glUniformLocation
STATIC_VERT
BLUR1D_FRAG
COPY_FRAG
GAME_FRAG
GLARE_FRAG
LASER_FRAG
PERSISTENCE_FRAG
PLAYER_FRAG
*/
var ctx,
gameCtx = g.getContext("2d"),
FW = MOBILE ? 480 : 800,
FH = MOBILE ? 660 : 680,
GAME_MARGIN = MOBILE ? 50 : 120,
GAME_Y_MARGIN = MOBILE ? 140 : GAME_MARGIN,
GAME_INC_PADDING = MOBILE ? 40 : 80,
W = FW - 2 * GAME_MARGIN,
H = FH - 2 * GAME_Y_MARGIN,
borderLength = 2*(W+H+2*GAME_INC_PADDING),
SEED = Math.random();
// DOM setup
d.style.webkitTransformOrigin = d.style.transformOrigin = "0 0";
g.width = W;
g.height = H;
var uiScale = 1;
function checkSize () {
}

View File

@ -0,0 +1,18 @@
precision highp float;
varying vec2 uv;
uniform sampler2D t;
uniform vec2 dim;
uniform vec2 dir;
void main() {
vec4 color = vec4(0.0);
vec2 off1 = vec2(1.3846153846) * dir;
vec2 off2 = vec2(3.2307692308) * dir;
color += texture2D(t, uv) * 0.2270270270;
color += texture2D(t, uv + (off1 / dim)) * 0.3162162162;
color += texture2D(t, uv - (off1 / dim)) * 0.3162162162;
color += texture2D(t, uv + (off2 / dim)) * 0.0702702703;
color += texture2D(t, uv - (off2 / dim)) * 0.0702702703;
gl_FragColor = color;
}

View File

@ -0,0 +1,8 @@
precision highp float;
varying vec2 uv;
uniform sampler2D t;
void main() {
gl_FragColor = texture2D(t, uv);
}

View File

@ -0,0 +1,46 @@
precision highp float;
varying vec2 uv;
uniform sampler2D G; // game
uniform sampler2D R; // persistence
uniform sampler2D B; // blur
uniform sampler2D L; // glare
uniform sampler2D E; // env (player)
uniform float s; // starting
uniform float F; // fail factor (red effect)
uniform vec2 k;
float squircleDist (vec2 a, vec2 b) {
float p = 10.0;
vec2 c = a-b;
return pow(abs(pow(abs(c.x), p)+pow(abs(c.y), p)), 1.0/p);
}
void main() {
vec2 UV = uv + k;
vec2 pos = (UV/0.98)-0.01;
float d = squircleDist(UV, vec2(0.5));
float dd = smoothstep(0.45, 0.51, d);
pos = mix(pos, vec2(0.5), 0.2 * (0.6 - d) - 0.02 * d);
vec3 gc = texture2D(G, pos).rgb;
gl_FragColor =
step(0.0, UV.x) *
step(UV.x, 1.0) *
step(0.0, UV.y) *
step(UV.y, 1.0) *
vec4((
vec3(0.03 + 0.1 * F, 0.04, 0.05) +
mix(vec3(0.05, 0.1, 0.15) - gc, 2.0 * gc, s) +
s * (
texture2D(L, pos).rgb +
vec3(0.3 + F, 0.6, 1.0) * (
texture2D(R, pos).rgb +
3.0 * texture2D(B, pos).rgb
) +
0.5 * texture2D(E, pos).rgb
)
)
* mix(1.0, smoothstep(1.0, 0.0, dd), 0.6), 1.0);
}

View File

@ -0,0 +1,8 @@
precision highp float;
varying vec2 uv;
uniform sampler2D t;
void main() {
gl_FragColor = vec4(step(0.9, texture2D(t, uv).r));
}

View File

@ -0,0 +1,15 @@
precision highp float;
varying vec2 uv;
uniform sampler2D t;
void main() {
vec3 c = texture2D(t, uv).rgb;
vec2 off = 0.003 * vec2(
cos(47.0 * uv.y),
sin(67.0 * uv.x)
);
gl_FragColor = vec4(
c.r + c.g + c.b + texture2D(t, uv+off).b
);
}

View File

@ -0,0 +1,13 @@
precision highp float;
varying vec2 uv;
uniform sampler2D t;
uniform sampler2D r;
void main() {
vec3 b = texture2D(r, uv).rgb;
gl_FragColor = vec4(
b * (0.82 - 0.3 * b.r * b.r) +
texture2D(t, uv).rgb,
1.0);
}

View File

@ -0,0 +1,107 @@
precision highp float;
varying vec2 uv;
uniform float pt; // playing since time
uniform float pl; // player number
uniform float S; // Seed
uniform float ex; // excitement
uniform float J; // jump
uniform float P; // playing
float disc (vec2 c, vec2 r) {
return step(length((uv - c) / r), 1.0);
}
float squircle (vec2 c, vec2 r, float p) {
vec2 v = (uv - c) / r;
return step(pow(abs(v.x), p) + pow(abs(v.y), p), 1.0);
}
vec3 env () {
return 0.1 +
0.3 * vec3(1.0, 0.9, 0.7) * smoothstep(0.4, 0.1, distance(uv, vec2(0.2, 1.2))) +
0.4 * vec3(0.8, 0.6, 1.0) * smoothstep(0.5, 0.2, distance(uv, vec2(1.3, 0.7)));
}
vec4 player (float p, float dx) {
vec4 c = vec4(0.0);
vec2 e = vec2(
min(ex, 1.0),
mix(min(ex, 1.0), min(ex-1.0, 1.0), 0.5));
// variable params
vec4 skin = 0.2 + 0.4 * pow(abs(cos(4.*p+S)), 2.0) * vec4(1.0, 0.7, 0.3, 1.0);
vec4 hair = vec4(0.5, 0.3, 0.3, 1.0);
vec4 sweater = vec4(
0.3 * (1.0 + cos(3.*p + 6.*S)),
0.2 * (1.0 + cos(7.*p + 7.*S)),
0.1+0.2 * (1.0 + sin(7.*p + 8.*S)),
1.0);
float feminity = step(sin(9.0*p+S), 0.0);
float hairSize = 0.02 + 0.02 * feminity * cos(p+S);
float walk = step(dx, -0.01) + step(0.01, dx);
float play = (1.0 - walk) * step(0.0, pt);
vec2 pos = vec2(0.5) +
// jumping cycle
J * vec2(0.0, 0.2) +
// walking cycle
walk * vec2(
0.03 * cos(4.0*pt + sin(pt)),
0.05 * abs(sin(3.0*pt))) +
// playing cycle
e * play * (1.0 - P) * vec2(
0.05 * cos(pt * (1.0 + 0.1 * sin(pt))),
0.05 * abs(sin(pt)));
vec2 pos2 = mix(pos, vec2(0.5), 0.5);
pos.x += dx;
pos2.x += dx;
// face skin
c += skin * disc(pos, vec2(0.06, 0.1));
// mouth
c *= 1.0 - (0.5 + 0.5 * feminity) * disc(pos - vec2(0.0, 0.04), vec2(0.03, 0.01));
// left eye
c *= 1.0 - disc(pos + vec2(0.03, 0.03), vec2(0.02, 0.01));
// right eye
c *= 1.0 - disc(pos + vec2(-0.03, 0.03), vec2(0.02, 0.01));
// nose
c *= 1.0 - 0.6 * disc(pos, vec2(0.01, 0.02));
// hair (also contrib to face skin color)
c += hair * disc(pos + vec2(0.0, hairSize), vec2(0.07, 0.1 + hairSize));
// left hand
c += play * (hair + skin) * disc(pos2 - vec2(
-0.2 + 0.01 * cos(5.0*pt),
0.45 - 0.1 * e.y * step(0.0, pt) * P * pow(abs(sin(8.0 * pt * (1.0 + 0.2 * cos(pt)))), 4.0)
), vec2(0.055, 0.05));
// right hand
c += play * (hair + skin) * disc(pos2 - vec2(
0.2 + 0.01 * cos(5.0*pt),
0.45 - 0.1 * e.x * step(2.0, pt) * P * pow(abs(cos(7.0 * pt)), 4.0)
), vec2(0.055, 0.05));
// neck
c += step(c.a, 0.0) * (hair + skin) *
squircle(pos - vec2(0.0, 0.10 + 0.02 * feminity),
vec2(0.05 - 0.01 * feminity, 0.03), 4.0);
// sweater
vec2 sr = vec2(
0.16 + 0.04 * sin(9.*p),
0.27 + 0.02 * cos(9.*p));
c += step(c.r+c.g+c.b, 0.0) * sweater * step(1.0,
squircle(pos - vec2(0.0, 0.35), sr * (1.0 - 0.1 * feminity), 4.0) +
disc(pos - vec2(0.0, 0.35), sr));
return c;
}
void main() {
float light = 0.6 + 0.4 * smoothstep(2.0, 0.0, distance(pt, -2.0));
vec4 c = vec4(0.0);
// main player
c += (1.0 - smoothstep(-0.0, -5.0, pt)) *
player(pl+step(pt, 0.0), -0.6 * smoothstep(-1., -5., pt));
// prev player
c += step (1.0, pl) *
player(pl+step(pt, 0.0)-1.0, 2.0 *smoothstep(-4., -1., pt));
c *= 1.0 - 1.3 * distance(uv, vec2(0.5));
gl_FragColor = vec4(light * mix(env(), c.rgb, clamp(c.a, 0.0, 1.0)), 1.0);
}

View File

@ -0,0 +1,7 @@
attribute vec2 p;
varying vec2 uv;
void main() {
gl_Position = vec4(p,0.0,1.0);
uv = 0.5 * (p+1.0);
}

View File

@ -0,0 +1,20 @@
/* global audio */
var Ashot = audio([0,0.06,0.18,,0.33,0.5,0.23,-0.04,-0.24,,,-0.02,,0.37,-0.22,,,,0.8,,,,,0.3]),
Amusic1 = audio([,,0.12,,0.13,0.16,,,,,,,,,,,,,0.7,,,,,0.5]),
Amusic2 = audio([,,0.12,,0.13,0.165,,,,,,,,,,,,,0.7,,,,,0.5]),
Aexplosion1 = audio([3,,0.35,0.5369,0.5,0.15,,-0.02,,,,-0.7444,0.78,,,0.7619,,,0.1,,,,,0.5]),
Aexplosion2 = audio([3,,0.38,0.5369,0.52,0.18,,-0.02,,,,-0.7444,0.78,,,0.7619,,,0.1,,,,,0.5]),
Asend = audio([2,0.07,0.04,,0.24,0.25,,0.34,-0.1999,,,-0.02,,0.3187,,,-0.14,0.04,0.85,,0.28,0.63,,0.5]),
AsendFail = audio([1,,0.04,,0.45,0.14,0.06,-0.06,0.02,0.87,0.95,-0.02,,0.319,,,-0.14,0.04,0.5,,,,,0.4]),
Alost = audio([0,0.11,0.37,,0.92,0.15,,-0.06,-0.04,0.3,0.14,0.1,,0.5047,,,,,0.16,-0.02,,0.5,,1]),
Aleave = audio([0,0.11,0.36,,0.66,0.19,,0.06,-0.06,0.05,0.8,-0.12,0.3,0.19,-0.06,,,-0.02,0.23,-0.02,,0.4,,0.4]),
Acoin = audio([0,,0.094,0.29,0.42,0.563,,,,,,0.4399,0.5658,,,,,,1,,,,,0.5]),
Amsg = audio([2,0.07,0.1,,0.2,0.75,0.35,-0.1,0.12,,,-0.02,,,,,-0.06,-0.0377,0.26,,,0.8,,0.7]),
Aufo = audio([2,0.05,0.74,,0.33,0.5,,,,0.46,0.29,,,,,,,,1,,,,,0.3]),
Alife = audio([0,0.12,0.8,0.48,0.77,0.92,,-0.12,-0.0999,,,-0.4,0.2,0.34,,0.65,,,0.93,-0.02,,,,0.38]),
Ajump = audio([3,,0.12,0.56,0.27,0.07,,-0.12,0.02,,,-0.02,0.68,,,,-0.04,-0.022,0.06,,,0.06,,0.5]);

View File

@ -0,0 +1,96 @@
/* global
ctx t path lifes play Alost AIboostSmoothed dying:true deads:true achievements killSmoothed:true
*/
function spaceshipDie() {
if (dying) return;
dying = t;
if (lifes == 1) {
play(Alost);
}
deads ++;
achievements[1] ++;
killSmoothed ++;
}
/*
function resetSpaceship () {
var x = W * (0.25 + 0.5 * Math.random());
var y = H * (0.25 + 0.5 * Math.random());
spaceship = [x, y, 0, 0];
}
*/
// RENDERING
function drawSpaceship (o) {
ctx.strokeStyle = "#f00";
ctx.globalAlpha = 0.4;
ctx.rotate(o[4]);
if (dying) {
ctx.lineWidth = 2;
var delta = (t-dying)/200;
path([
[-6, -6 - 0.5*delta],
[3, -3 - 0.9*delta]
]);
ctx.stroke();
if (delta < 8) {
path([
[3 + 0.4*delta, -3 - 0.8*delta],
[12 + 0.4*delta, 0 - 0.5*delta]
]);
ctx.stroke();
}
path([
[12, 0+0.4*delta],
[3, 3+delta]
]);
ctx.stroke();
if (delta < 9) {
path([
[1, 5 + delta],
[-6, 6 + delta]
]);
ctx.stroke();
}
if (delta < 7) {
path([
[-6 - delta, -6],
[-6 - delta, 6]
]);
ctx.stroke();
}
}
else {
path([
[-6, -6],
[ 12, 0],
[ -6, 6],
[ -5, 0]
]);
ctx.stroke();
if (AIboostSmoothed>0.2) {
path([
[-7, 2*Math.random()-1],
[-7 - 5*AIboostSmoothed, 4*Math.random()-2]
]);
ctx.stroke();
}
if (AIboostSmoothed<-0.2) {
path([
[2, -5],
[2 - 5 * AIboostSmoothed, -7],
,
[2, 5],
[2 - 5 * AIboostSmoothed, 7]
]);
ctx.stroke();
}
}
}

View File

@ -0,0 +1,69 @@
/* global W H */
// N.B: constants don't live here
var t = 0, dt,
spaceship = [ W/2, H/2, 0, 0, 0 ], // [x, y, velx, vely, rot]
asteroids = [], // array of [x, y, rot, vel, shape, lvl]
ufos = [], // array of [x, y, vx, vy, timeBeforeShot]
bullets = [], // array of [x, y, velx, vely, life, isAlien]
incomingObjects = [], // array of: [pos, vel, ang, force, rotVel, shape, lvl, key, rotAmp, rotAmpValid, explodeTime]
particles = [], // array of [x, y, rot, vel, life]
dying = 0,
resurrectionTime = 0,
best = 0,
score = 0, // current asteroids player score
scoreForLife, // will track the next score to win a life (10000, 20000, ...)
playingSince = -10000,
deads = 0,
player = 0,
lifes = 0,
AIshoot = 0, AIboost = 0, AIrotate = 0, AIexcitement = 0,
AIboostSmoothed = 0,
shaking = [0,0],
jumping = 0,
jumpingFreq = 0,
jumpingPhase = 0,
jumpingFreqSmoothed = 0,
jumpingAmp = 0,
jumpingAmpSmoothed = 0,
killSmoothed = 0,
musicPhase = 0,
musicTick = 0,
musicPaused = 0,
ufoMusicTime = 0,
excitementSmoothed = 0,
neverPlayed = 1,
neverUFOs = 1,
combos = 0,
combosTarget,
gameOver,
awaitingContinue = 0,//localStorage.ba_pl && parseInt(localStorage.ba_pl),
// achievements: [nbAsteroids, nbKills, nbUfos]
achievements,
lastScoreIncrement = 0,
lastJump = 0,
lastBulletShoot = 0,
lastExtraLife = 0,
lastLoseShot = 0,
// Input state : updated by user events, handled & emptied by the update loop
keys = {},
tap,
// variables related to setup
gameScale;
function helpVisible () {
return neverPlayed &&
incomingObjects[0] &&
playingSince>8000;
}

View File

@ -0,0 +1,29 @@
<!doctype html>
<head>
<title>Behind Asteroids</title>
<meta name=viewport content="width=480, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta charset="utf-8">
<style>
body {
background: #000;
margin: 0;
padding: 0;
}
#d {
position: absolute;
}
canvas {
position: absolute;
top: 0;
left: 0;
}
</style>
</head>
<body>
<div id="d"><canvas id="c"></canvas><canvas id="u"></canvas><canvas id="g" hidden></canvas></div>
<script src="b.js"></script>
</body>
</html>

View File

@ -0,0 +1,56 @@
/* global
dt dying spaceship shoot ctx path
*/
function applyUFOlogic (o) {
o[4] -= dt;
if (o[4]<0) {
o[4] = 500 + 300 * Math.random();
if (!dying) {
var target = Math.atan2(spaceship[1] - o[1], spaceship[0] - o[0]);
if (!o[2] || Math.random()<0.2) {
var randomAngle = 2*Math.PI*Math.random();
o[2] = 0.08 * Math.cos(randomAngle);
o[3] = 0.08 * Math.sin(randomAngle);
}
shoot(o, 0.3+0.1*Math.random(), target + 0.6 * Math.random() - 0.3);
}
}
}
// RENDERING
var UFOa = [
[8,0],
[7,5],
[0,9],
[7,14]
];
var UFOb = [
[15,14],
[22,9],
[15,5],
[14,0]
];
var UFO =
UFOa
.concat(UFOb)
.concat(UFOa)
.concat([,])
.concat(UFOb)
.concat([
,
[7,5],
[15,5],
,
[0,9],
[22,9]
]);
function drawUFO () {
ctx.globalAlpha = 0.4;
ctx.strokeStyle = "#f00";
path(UFO);
ctx.stroke();
}

View File

@ -0,0 +1,430 @@
/* global
ctx path font gameOver W H player playingSince:true awaitingContinue scoreTxt
lifes dying MOBILE best score t UFO neverPlayed lastExtraLife neverUFOs dt play
Amsg GAME_MARGIN FW FH combos achievements musicTick helpVisible */
// IN GAME UI
function button (t1, t2) {
ctx.globalAlpha = 1;
path([
[0, 0],
[160, 0],
[160, 120],
[0, 120]
]);
ctx.translate(80, 30);
ctx.stroke();
ctx.fillStyle = "#000";
ctx.fill();
ctx.save();
font(t1, 2);
ctx.restore();
ctx.save();
ctx.translate(0, 40);
font(t2, 2);
ctx.restore();
}
function drawGameUI () {
ctx.save();
ctx.fillStyle = ctx.strokeStyle = "#0f0";
ctx.globalAlpha = 0.3;
if (gameOver) {
ctx.save();
ctx.strokeStyle = "#0f0";
ctx.globalAlpha = 0.3;
ctx.save();
ctx.translate((W-340)/2, H/8);
font("YOU EARNED ", 2, 1);
ctx.globalAlpha = 0.5;
font((player*25)+"¢", 2, 1);
ctx.restore();
ctx.save();
ctx.translate(W/2, H/4);
font("FROM "+player+" PLAYERS", 2);
ctx.restore();
ctx.save();
ctx.globalAlpha = 0.5;
ctx.translate((W-200)/2, H/2);
drawAchievements(2);
ctx.restore();
ctx.save();
ctx.translate(W/2 - 180, H - 160);
button("TWEET", "SCORE");
ctx.restore();
ctx.save();
ctx.translate(W/2 + 20, H - 160);
button("PLAY", "AGAIN");
ctx.restore();
ctx.restore();
}
else if (playingSince < 0 || awaitingContinue) {
ctx.save();
ctx.translate(W-50, 20);
font(scoreTxt(0), 1.5, -1);
ctx.restore();
ctx.save();
ctx.translate(50, 70);
if ((!awaitingContinue || playingSince>0) && t%1000<500)
font("PLAYER "+(awaitingContinue||player+1), 2, 1);
ctx.restore();
ctx.save();
ctx.translate(W/2 - 160, 0.7*H);
path([
[0,2],
[0,18]
]);
ctx.stroke();
ctx.translate(40,0);
font("COIN", 2, 1);
ctx.translate(40,0);
path([
[0,2],
[0,18]
]);
ctx.stroke();
ctx.translate(40,0);
font("PLAY", 2, 1);
ctx.restore();
}
else {
for (var i=1; i<lifes; i++) {
ctx.save();
ctx.translate(60 + i * 10, 50);
ctx.rotate(-Math.PI/2);
path([
[-4, -4],
[ 10, 0],
[ -4, 4],
[ -3, 0]
]);
ctx.stroke();
ctx.restore();
}
}
if (!gameOver && dying && lifes==1) {
ctx.save();
ctx.lineWidth = 2;
ctx.translate(W/2, 140);
font("GAME OVER", 2);
ctx.restore();
}
if (!gameOver && awaitingContinue && playingSince > 0) {
ctx.save();
ctx.globalAlpha = 1;
ctx.translate(W/2, 140);
font("CONTINUE ?", 3);
ctx.restore();
ctx.save();
ctx.globalAlpha = 1;
ctx.translate(W/4, 210);
font("YES", MOBILE ? 4 : 6);
ctx.restore();
ctx.save();
ctx.globalAlpha = 1;
ctx.translate(3*W/4, 210);
font("NO", MOBILE ? 4 : 6);
ctx.restore();
}
ctx.save();
ctx.translate(W/2, H-14);
font("2015 GREWEB INC", .6);
ctx.restore();
if (!gameOver) {
ctx.save();
ctx.translate(W/2, 20);
font(scoreTxt(best), .6);
ctx.restore();
ctx.save();
ctx.translate(50, 20);
font(scoreTxt(score), 1.5, 1);
ctx.restore();
}
if (gameOver || playingSince<0 && t%1000<800) {
ctx.save();
ctx.translate(W-20, H-24);
font(MOBILE ? "MOBILE" : "DESKTOP", .6, -1);
ctx.restore();
ctx.save();
ctx.translate(W-20, H-14);
font("VERSION", .6, -1);
ctx.restore();
}
ctx.restore();
}
function drawGlitch () {
ctx.save();
ctx.fillStyle =
ctx.strokeStyle = "#f00";
ctx.globalAlpha = 0.03;
ctx.translate(W/2, H/2);
ctx.beginPath();
ctx.arc(0, 0, 4, 0, 2*Math.PI);
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, 12, 0, 2*Math.PI);
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, 12, 4, 6);
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, 12, 1, 2);
ctx.stroke();
ctx.restore();
}
// EXTERNAL UI
var badgesIcons = [
[
[-11, -11],
[4, -13],
[6, -6],
[14, 0],
[14, 8],
[6, 8],
[-6, 14],
[-14, 0]
],
[
[-8, 13],
[0, -13],
[8, 13],
[0, 11],
[-8, 13],
,
[-10, -2],
[10, 2],
,
[10, -2],
[-10, 2],
,
],
UFO.map(function (p) {
return p ? [p[0]-11,p[1]-7] : p;
})
];
var lastStatement, lastStatementTime = 0;
var lastMessage2;
function drawUI () {
var currentMessage = "",
currentMessage2 = "",
currentMessageClr = "#f7c",
currentMessageClr2 = "#7fc";
function announcePlayer (player) {
currentMessage = "PLAYER "+player;
currentMessage2 = [
"GENIOUS PLAYER!!",
"EXPERIENCED PLAYER!!",
"GOOD PLAYER. GET READY",
"NICE PLAYER.",
"BEGINNER.",
"VERY BEGINNER. EASY KILL"
][Math.floor(Math.exp((-player)/8)*6)];
}
if (gameOver) {
currentMessage = "PLAYER MASTERED THE GAME";
currentMessage2 = "REACHED ᐃᐃᐃᐃᐃ";
}
else if (!player) {
if (playingSince<-7000) {
currentMessage = "BEHIND ASTEROIDS";
currentMessage2 = "THE DARK SIDE";
}
else if (playingSince<-3500) {
currentMessageClr = currentMessageClr2 = "#7cf";
currentMessage = "SEND ASTEROIDS TO MAKE";
currentMessage2 = "PLAYERS WASTE THEIR MONEY";
}
else if (!awaitingContinue) {
var nb = Math.min(25, Math.floor((playingSince+3500)/80));
for (var i=0; i<nb; i++)
currentMessage += ".";
if (playingSince>-2000)
currentMessage2 = "A NEW PLAYER!";
}
else {
if (playingSince<0) playingSince = 0; // jump to skip the "player coming"
announcePlayer(awaitingContinue);
}
}
else if (dying) {
if (lifes==1) {
currentMessageClr2 = "#f66";
currentMessage = "GOOD JOB !!!";
currentMessage2 = "THE DUDE IS BROKE";
}
else if (lifes==2) {
currentMessageClr2 = "#f66";
currentMessage = "OK...";
currentMessage2 = "ONE MORE TIME !";
}
else {
if (lastStatement && t - lastStatementTime > 3000) { // lastStatementTime is not used here
currentMessage = lastStatement;
}
else {
currentMessage = ["!!!", "GREAT!", "COOL!", "OMG!", "AHAH!", "RUDE!", "EPIC!", "WICKED!", "SHAME!", "HEHEHE!", "BWAHAHA!"];
lastStatement = currentMessage = currentMessage[Math.floor(Math.random() * currentMessage.length)];
lastStatementTime = 0;
}
}
}
else {
if (playingSince<0) {
currentMessage = "INCOMING NEW PLAYER...";
currentMessage2 = "25¢ 25¢ 25¢ 25¢ 25¢";
}
else if (playingSince<6000 && lifes==4) {
announcePlayer(player);
}
else {
currentMessageClr2 = "#f66";
if (lastStatement && t - lastStatementTime < 3000) {
currentMessage2 = lastStatement;
}
else {
if (neverPlayed) {
if (helpVisible()) {
currentMessageClr = currentMessageClr2 = "#f7c";
currentMessage = MOBILE ? "TAP ON ASTEROIDS" : "PRESS ASTEROIDS LETTER";
currentMessage2 = "TO SEND THEM TO THE GAME";
}
}
else if (lifes > 4 && t - lastExtraLife > 5000) {
currentMessageClr = currentMessageClr2 = "#f66";
currentMessage = "DON'T LET PLAYER";
currentMessage2 = "REACH ᐃᐃᐃᐃᐃ !!!";
}
else if (score > 10000 && t - lastExtraLife < 4500) {
currentMessageClr = currentMessageClr2 = "#f66";
currentMessage = "OH NO! PLAYER JUST";
currentMessage2 = "WON AN EXTRA LIFE!";
}
else if (player==2 && 5000<playingSince) {
currentMessageClr2 = currentMessageClr = "#7cf";
currentMessage = "LETS TRAIN WITH...";
currentMessage2 = "AIMING";
}
else if (player==3 && 5000<playingSince) {
currentMessageClr = "#7cf";
currentMessageClr2 = "#f66";
currentMessage = "CAREFUL ABOUT THE";
currentMessage2 = "RED AIMING";
}
else if (player==4 && 5000<playingSince && neverUFOs) {
currentMessageClr = currentMessageClr2 = "#f7c";
currentMessage = "MAKE COMBOS TO SEND";
currentMessage2 = "AN UFO !!!";
}
else if (player > 5) {
lastStatement = 0;
if (Math.random() < 0.0001 * dt && t - lastStatementTime > 8000) {
currentMessage2 = [
"COME ON! KILL IT!",
"JUST DO IT!",
"I WANT ¢¢¢",
"GIVE ME SOME ¢¢¢",
"DO IT!",
"DESTROY IT!"
];
lastStatement = currentMessage2 = currentMessage2[Math.floor(Math.random() * currentMessage2.length)];
lastStatementTime = t;
}
}
}
}
}
if (currentMessage2 && lastMessage2 !== currentMessage2 &&
(currentMessageClr2 == "#f66" || currentMessageClr2 == "#f7c")) {
play(Amsg);
}
ctx.save();
ctx.translate(GAME_MARGIN, MOBILE ? 40 : 2);
ctx.lineWidth = (t%600>300) ? 2 : 1;
ctx.save();
ctx.strokeStyle = currentMessageClr;
font(currentMessage, MOBILE ? 1.5 : 2, 1);
ctx.restore();
ctx.save();
ctx.strokeStyle = currentMessageClr2;
ctx.translate(0, MOBILE ? 30 : 40);
font(lastMessage2 = currentMessage2, MOBILE ? 1.5 : 2, 1);
ctx.restore();
ctx.restore();
if (gameOver) return;
ctx.save();
ctx.translate(FW - GAME_MARGIN, 2);
ctx.lineWidth = 2;
ctx.strokeStyle = "#7cf";
font(((playingSince>0&&awaitingContinue||player)*25)+"¢", 2, -1);
ctx.restore();
ctx.save();
ctx.globalAlpha = musicTick ? 1 : 0.6;
ctx.strokeStyle = "#7cf";
ctx.translate(FW - GAME_MARGIN, FH - 30);
if (combos) font(combos+"x", 1.5, -1);
ctx.restore();
/*
if (combos && combosTarget-combos < 9) {
ctx.save();
ctx.strokeStyle = "#7cf";
ctx.globalAlpha = musicTick ? 1 : 0.5;
ctx.translate(FW - GAME_MARGIN, FH - 50);
font((1+combosTarget-combos)+" ", 1, -1);
ctx.translate(0, 0);
path(UFO);
ctx.stroke();
ctx.restore();
}
*/
if (achievements) {
ctx.save();
ctx.translate(GAME_MARGIN + 50, FH - 20);
ctx.strokeStyle = "#fc7";
drawAchievements(1);
ctx.restore();
}
}
function drawAchievements (fontSize) {
for (var j = 0; j < 3; j++) {
var badge = achievements[j];
if (badge) {
ctx.save();
ctx.translate(100 * j, 0);
path(badgesIcons[j]);
ctx.stroke();
ctx.translate(0, -20 - 10 * fontSize);
font(""+badge, fontSize);
ctx.restore();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,339 @@
//@flow
import React, { Component } from "react";
import { Shaders, Node, GLSL, Bus, Backbuffer } from "gl-react";
import { Surface } from "gl-react-dom";
import gameBuild from "./build";
/**
* This example reproduce the after effects made in a js13k game:
* https://github.com/gre/behind-asteroids
* see also https://github.com/gre/behind-asteroids/blob/master/src/effects.js
*/
const shaders = Shaders.create({
blur1d: {
frag: GLSL`
precision highp float;
varying vec2 uv;
uniform sampler2D t;
uniform vec2 dim;
uniform vec2 dir;
void main() {
vec4 color = vec4(0.0);
vec2 off1 = vec2(1.3846153846) * dir;
vec2 off2 = vec2(3.2307692308) * dir;
color += texture2D(t, uv) * 0.2270270270;
color += texture2D(t, uv + (off1 / dim)) * 0.3162162162;
color += texture2D(t, uv - (off1 / dim)) * 0.3162162162;
color += texture2D(t, uv + (off2 / dim)) * 0.0702702703;
color += texture2D(t, uv - (off2 / dim)) * 0.0702702703;
gl_FragColor = color;
}
`
},
game: {
frag: GLSL`
precision highp float;
varying vec2 uv;
uniform sampler2D G;
uniform sampler2D R;
uniform sampler2D B;
uniform sampler2D L;
uniform sampler2D E;
uniform float s;
uniform float F;
uniform vec2 k;
float squircleDist (vec2 a, vec2 b) {
float p = 10.0;
vec2 c = a-b;
return pow(abs(pow(abs(c.x), p)+pow(abs(c.y), p)), 1.0/p);
}
void main() {
vec2 UV = uv + k;
vec2 pos = (UV/0.98)-0.01;
float d = squircleDist(UV, vec2(0.5));
float dd = smoothstep(0.45, 0.51, d);
pos = mix(pos, vec2(0.5), 0.2 * (0.6 - d) - 0.02 * d);
vec3 gc = texture2D(G, pos).rgb;
gl_FragColor =
step(0.0, UV.x) *
step(UV.x, 1.0) *
step(0.0, UV.y) *
step(UV.y, 1.0) *
vec4((
vec3(0.03 + 0.1 * F, 0.04, 0.05) +
mix(vec3(0.05, 0.1, 0.15) - gc, 2.0 * gc, s) +
s * (
texture2D(L, pos).rgb +
vec3(0.3 + F, 0.6, 1.0) * (
texture2D(R, pos).rgb +
3.0 * texture2D(B, pos).rgb
) +
0.5 * texture2D(E, pos).rgb
)
)
* mix(1.0, smoothstep(1.0, 0.0, dd), 0.6), 1.0);
}
`
},
glare: {
frag: GLSL`
precision highp float;
varying vec2 uv;
uniform sampler2D t;
void main() {
gl_FragColor = vec4(step(0.9, texture2D(t, uv).r));
}
`
},
laser: {
frag: GLSL`
precision highp float;
varying vec2 uv;
uniform sampler2D t;
void main() {
vec3 c = texture2D(t, uv).rgb;
vec2 off = 0.003 * vec2(
cos(47.0 * uv.y),
sin(67.0 * uv.x)
);
gl_FragColor = vec4(
c.r + c.g + c.b + texture2D(t, uv+off).b
);
}
`
},
persistence: {
frag: GLSL`
precision highp float;
varying vec2 uv;
uniform sampler2D t;
uniform sampler2D r;
void main() {
vec3 b = texture2D(r, uv).rgb;
gl_FragColor = vec4(
b * (0.82 - 0.3 * b.r * b.r) +
texture2D(t, uv).rgb,
1.0);
}
`
},
player: {
frag: GLSL`
precision highp float;
varying vec2 uv;
uniform float pt;
uniform float pl;
uniform float S;
uniform float ex;
uniform float J;
uniform float P;
float disc (vec2 c, vec2 r) {
return step(length((uv - c) / r), 1.0);
}
float squircle (vec2 c, vec2 r, float p) {
vec2 v = (uv - c) / r;
return step(pow(abs(v.x), p) + pow(abs(v.y), p), 1.0);
}
vec3 env () {
return 0.1 +
0.3 * vec3(1.0, 0.9, 0.7) *
smoothstep(0.4, 0.1, distance(uv, vec2(0.2, 1.2))) +
0.4 * vec3(0.8, 0.6, 1.0) *
smoothstep(0.5, 0.2, distance(uv, vec2(1.3, 0.7)));
}
vec4 player (float p, float dx) {
vec4 c = vec4(0.0);
vec2 e = vec2(
min(ex, 1.0),
mix(min(ex, 1.0), min(ex-1.0, 1.0), 0.5));
vec4 skin = 0.2 + 0.4 * pow(abs(cos(4.*p+S)), 2.0) *
vec4(1.0, 0.7, 0.3, 1.0);
vec4 hair = vec4(0.5, 0.3, 0.3, 1.0);
vec4 sweater = vec4(
0.3 * (1.0 + cos(3.*p + 6.*S)),
0.2 * (1.0 + cos(7.*p + 7.*S)),
0.1+0.2 * (1.0 + sin(7.*p + 8.*S)),
1.0);
float feminity = step(sin(9.0*p+S), 0.0);
float hairSize = 0.02 + 0.02 * feminity * cos(p+S);
float walk = step(dx, -0.01) + step(0.01, dx);
float play = (1.0 - walk) * step(0.0, pt);
vec2 pos = vec2(0.5) +
J * vec2(0.0, 0.2) +
walk * vec2(
0.03 * cos(4.0*pt + sin(pt)),
0.05 * abs(sin(3.0*pt))) +
e * play * (1.0 - P) * vec2(
0.05 * cos(pt * (1.0 + 0.1 * sin(pt))),
0.05 * abs(sin(pt)));
vec2 pos2 = mix(pos, vec2(0.5), 0.5);
pos.x += dx;
pos2.x += dx;
c += skin * disc(pos, vec2(0.06, 0.1));
c *= 1.0 - (0.5 + 0.5 * feminity) *
disc(pos - vec2(0.0, 0.04), vec2(0.03, 0.01));
c *= 1.0 - disc(pos + vec2(0.03, 0.03), vec2(0.02, 0.01));
c *= 1.0 - disc(pos + vec2(-0.03, 0.03), vec2(0.02, 0.01));
c *= 1.0 - 0.6 * disc(pos, vec2(0.01, 0.02));
c += hair * disc(pos + vec2(0.0, hairSize), vec2(0.07, 0.1 + hairSize));
c += play * (hair + skin) * disc(pos2 - vec2(
-0.2 + 0.01 * cos(5.0*pt),
0.45 - 0.1 * e.y * step(0.0, pt) * P * pow(abs(sin(8.0 * pt *
(1.0 + 0.2 * cos(pt)))), 4.0)
), vec2(0.055, 0.05));
c += play * (hair + skin) * disc(pos2 - vec2(
0.2 + 0.01 * cos(5.0*pt),
0.45 - 0.1 * e.x * step(2.0, pt) * P * pow(abs(cos(7.0 * pt)), 4.0)
), vec2(0.055, 0.05));
c += step(c.a, 0.0) * (hair + skin) *
squircle(pos - vec2(0.0, 0.10 + 0.02 * feminity),
vec2(0.05 - 0.01 * feminity, 0.03), 4.0);
vec2 sr = vec2(
0.16 + 0.04 * sin(9.*p),
0.27 + 0.02 * cos(9.*p));
c += step(c.r+c.g+c.b, 0.0) * sweater * step(1.0,
squircle(pos - vec2(0.0, 0.35), sr * (1.0 - 0.1 * feminity), 4.0) +
disc(pos - vec2(0.0, 0.35), sr));
return c;
}
void main() {
float light = 0.6 + 0.4 * smoothstep(2.0, 0.0, distance(pt, -2.0));
vec4 c = vec4(0.0);
c += (1.0 - smoothstep(-0.0, -5.0, pt)) *
player(pl+step(pt, 0.0), -0.6 * smoothstep(-1., -5., pt));
c += step (1.0, pl) *
player(pl+step(pt, 0.0)-1.0, 2.0 *smoothstep(-4., -1., pt));
c *= 1.0 - 1.3 * distance(uv, vec2(0.5));
gl_FragColor = vec4(light * mix(env(), c.rgb, clamp(c.a, 0.0, 1.0)), 1.0);
}
`
}
});
const Blur1D =
({ dim, dir, children: t }) =>
<Node shader={shaders.blur1d} uniforms={{ dim, dir, t }} />;
export default class Example extends Component {
render () {
const {showCanvas} = this.props;
const {pt,pl,ex,J,P,s,F,k,S,W,H} =
// HACK to just render the game
this._ ? this._.getWebGLParams() :
({ pt: 0, pl: 0, ex: 0, J: 0, P: 0, s: 0, F: 0, k: [0,0], W: 2, H: 2, S: 0 });
const dim = [ W, H ];
return (
<div style={{ background: "black" }} ref="container">
<Surface width={W} height={H} pixelRatio={1}>
<Bus ref="laser">
<Node
shader={shaders.laser}
uniforms={{ t: () => this.refs.gameCanvas }}
/>
</Bus>
<Bus ref="player">
<Blur1D dim={dim} dir={[ 0, 2 ]}>
<Blur1D dim={dim} dir={[ 6, 0 ]}>
<Blur1D dim={dim} dir={[ 2, 2 ]}>
<Blur1D dim={dim} dir={[ -2, 2 ]}>
<Node
shader={shaders.player}
uniforms={{ pt, pl, ex, J, P, S }}
/>
</Blur1D>
</Blur1D>
</Blur1D>
</Blur1D>
</Bus>
<Bus ref="glare">
<Blur1D dim={dim} dir={[ 2, -4 ]}>
<Node
shader={shaders.glare}
uniforms={{ t: () => this.refs.laser }}
/>
</Blur1D>
</Bus>
<Bus ref="glareCursor">
<Blur1D dim={dim} dir={[ 4, -8 ]}>
{() => this.refs.glare}
</Blur1D>
</Bus>
<Bus ref="glareBlurred">
<Blur1D dim={dim} dir={[ 0, 1 ]}>
<Blur1D dim={dim} dir={[ 1, 0 ]}>
<Blur1D dim={dim} dir={[ -0.5, 0.5 ]}>
<Blur1D dim={dim} dir={[ 0.5, 0.5 ]}>
{() => this.refs.laser
//FIXME this should be glare instead.
//but i think there is a bug in gl-react!
}
</Blur1D>
</Blur1D>
</Blur1D>
</Blur1D>
</Bus>
<Bus ref="persistence">
<Node
shader={shaders.persistence}
backbuffering
uniforms={{
t: this.refs.glareBlurred,
r: Backbuffer
}}
/>
</Bus>
<Node
shader={shaders.game}
uniforms={{
G: () => this.refs.laser,
R: () => this.refs.persistence,
B: () => this.refs.glareBlurred,
L: () => this.refs.glareCursor,
E: () => this.refs.player,
s,
F,
k
}} />
</Surface>
<canvas id="c" ref="gameCanvas" hidden={!showCanvas} />
<div style={{ textAlign: "center", padding: 20 }}>
<button onClick={this.sendAsteroid}>
SEND ASTEROID!
</button>
</div>
</div>
);
}
_: any;
componentDidMount () {
this._ = gameBuild(
this.refs.container,
this.refs.gameCanvas,
() => this.forceUpdate()
);
}
componentWillUnmount() {
this._.dispose();
}
sendAsteroid = () => window._behindAsteroids_send();
static defaultProps = {
showCanvas: false,
};
}

View File

@ -0,0 +1,18 @@
import React from "react";
export const title = "Behind Asteroids (js13k 2015 greweb)";
export const toolbox = [
{ prop: "showCanvas",
title: "Under the hood",
Editor:
({ value, onChange }) =>
<div>
<label>
<input type="checkbox" checked={value} onChange={e => onChange(e.target.checked)} />
Show the 2D Canvas rendered by the game
</label>
<p><em>The rest of the rendering is done in WebGL (ported in gl-react).</em></p>
</div>
},
];

View File

@ -0,0 +1,165 @@
//@flow
import React, { PureComponent, Component, PropTypes } from "react";
import { Shaders, Node, GLSL, Bus, LinearCopy } from "gl-react";
import { Surface } from "gl-react-dom";
import JSON2D from "react-json2d";
import {Blur1D} from "../blurxy";
import {Blur} from "../blurmulti";
import {BlurV} from "../blurmap";
const shaders = Shaders.create({
ImageTitle: {
frag: GLSL`
precision highp float;
varying vec2 uv;
uniform sampler2D img, imgBlurred, imgTone, title, blurMap;
uniform float colorThreshold;
float monochrome (vec3 c) {
return 0.2125 * c.r + 0.7154 * c.g + 0.0721 * c.b;
}
void main() {
float blurFactor = texture2D(blurMap, uv).r;
vec4 bgColor = mix(
texture2D(img, uv),
texture2D(imgBlurred, uv),
step(0.01, blurFactor)
);
vec4 textColor = vec4(vec3(
step(monochrome(texture2D(imgTone, uv).rgb), colorThreshold)
), 1.0);
float isText = 1.0 - texture2D(title, uv).r;
gl_FragColor = mix(bgColor, textColor, isText);
}`
},
TitleBlurMap: {
frag: GLSL`
precision highp float;
varying vec2 uv;
uniform sampler2D t;
uniform float threshold;
void main() {
gl_FragColor = vec4(
vec3(smoothstep(1.0, threshold, texture2D(t, uv).r)),
1.0);
}`
},
});
const AveragePixels = ({ children, quality }) =>
<Blur1D
width={1}
height={1}
resolution={[ 1, 1 ]}
direction={[ 0, 0.1 ]}>
<Blur1D
width={1}
height={quality}
resolution={[ 1, quality ]}
direction={[ 0.1, 0 ]}>
{children}
</Blur1D>
</Blur1D>;
const TitleBlurMap = ({ children: title, threshold }) =>
<Node
shader={shaders.TitleBlurMap}
uniforms={{
threshold,
t:
<Blur factor={4} passes={4} width={200} height={200}>
{title}
</Blur>
}}
width={64}
height={64}
/>;
class Title extends PureComponent {
render () {
const { children, width, height } = this.props;
return <LinearCopy><JSON2D width={width} height={height}>{{
size: [ width, height ],
background: "#fff",
draws: [
{
"font": "bold 78px Didot,Georgia,serif",
"fillStyle": "#000",
"textBaseline": "top",
"textAlign": "center"
},
[ "fillText", children, width/2, 10, 84 ],
]
}}</JSON2D></LinearCopy>;
}
}
class ImageTitle extends Component {
static contextTypes = {
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
};
render() {
const { children: img, text, colorThreshold } = this.props;
const { width, height } = this.context;
return (
<Node
shader={shaders.ImageTitle}
uniforms={{
colorThreshold,
img,
imgBlurred:() => this.refs.imgBlurred,
title:() => this.refs.title,
imgTone:() => this.refs.imgTone,
blurMap:() => this.refs.blurMap,
}}>
<Bus ref="title">
<Title width={width} height={height}>
{text}
</Title>
</Bus>
<Bus ref="blurMap">
<TitleBlurMap threshold={0.7}>
{() => this.refs.title}
</TitleBlurMap>
</Bus>
<Bus ref="imgTone">
<AveragePixels quality={8}>
{img}
</AveragePixels>
</Bus>
<Bus ref="imgBlurred">
<BlurV
map={() => this.refs.blurMap}
factor={4}
passes={4}>
{img}
</BlurV>
</Bus>
</Node>
);
}
}
export default class Example extends Component {
render() {
const { title, colorThreshold, image } = this.props;
return (
<Surface width={450} height={300}>
<ImageTitle text={title} colorThreshold={colorThreshold}>
{image}
</ImageTitle>
</Surface>
);
}
static defaultProps = {
title: "Hello\nSan Francisco\n☻",
colorThreshold: 0.6,
image: require("./sf-6.jpg"),
};
}

View File

@ -0,0 +1,36 @@
import ImagesPicker from "../../toolbox/ImagesPicker";
import makeTextArea from "../../toolbox/makeTextArea";
import makeFloatSlider from "../../toolbox/makeFloatSlider";
export const title = "Dynamic Blur Image Title";
export const toolbox = [
{ prop: "title",
title: "Title",
Editor: makeTextArea({
height: 140,
padding: 6,
fontFamily: "Didot,Georgia,serif",
fontSize: "36px",
lineHeight: "42px",
fontWeight: "bold",
textAlign: "center",
}) },
{ prop: "colorThreshold",
title: "Color Threshold",
Editor: makeFloatSlider(0, 1, 0.01) }, // FIXME black <-> white
{ prop: "image",
title: "Image",
Editor: ImagesPicker,
style: { width: 400 },
imageStyle: { maxWidth: 56, maxHeight: 56, marginBottom: 16, },
images: [
require("./sf-1.jpg"),
require("./sf-2.jpg"),
require("./sf-3.jpg"),
require("./sf-4.jpg"),
require("./sf-5.jpg"),
require("./sf-6.jpg"),
require("./sf-7.jpg"),
] },
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

View File

@ -0,0 +1,68 @@
//@flow
import React, { Component } from "react";
import { Shaders, Node, GLSL, connectSize } from "gl-react";
import { Surface } from "gl-react-dom";
const shaders = Shaders.create({
blurV1D: {
frag: GLSL`precision highp float;
varying vec2 uv;
uniform sampler2D t, map;
uniform vec2 direction, resolution;
vec4 blur9(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) {
vec4 color = vec4(0.0);
vec2 off1 = vec2(1.3846153846) * direction;
vec2 off2 = vec2(3.2307692308) * direction;
color += texture2D(image, uv) * 0.2270270270;
color += texture2D(image, uv + (off1 / resolution)) * 0.3162162162;
color += texture2D(image, uv - (off1 / resolution)) * 0.3162162162;
color += texture2D(image, uv + (off2 / resolution)) * 0.0702702703;
color += texture2D(image, uv - (off2 / resolution)) * 0.0702702703;
return color;
}
void main() {
gl_FragColor = blur9(t, uv, resolution, direction * texture2D(map, uv).rg);
}`
}
});
// Same concept than Blur1D except it takes one more prop:
// a map texture that tells the blur intensity for a given position.
export const BlurV1D =
connectSize(({ children: t, direction, map, width, height }) =>
<Node
shader={shaders.blurV1D}
uniforms={{ t, map, resolution: [ width, height ], direction }}
/>);
// And its N-pass version
import {directionForPass} from "../blurmulti";
export const BlurV =
connectSize(({ children, factor, map, passes }) => {
const rec = pass =>
pass <= 0
? children
: <BlurV1D map={map} direction={directionForPass(pass, factor, passes)}>
{rec(pass-1)}
</BlurV1D>;
return rec(passes);
});
export default class Example extends Component {
render() {
const { factor, passes, map } = this.props;
return (
<Surface width={600} height={284}>
<BlurV map={map} passes={passes} factor={factor}>
https://i.imgur.com/NjbLHx2.jpg
</BlurV>
</Surface>
);
}
static defaultProps = {
factor: 2,
passes: 4,
map: StaticBlurMap.images[0],
};
};
import StaticBlurMap from "../../toolbox/StaticBlurMap";

View File

@ -0,0 +1,21 @@
import markdown from "../../markdown";
import StaticBlurMap from "../../toolbox/StaticBlurMap";
import makeFloatSlider from "../../toolbox/makeFloatSlider";
export const title = "Blur with intensity map & multi-pass";
export const desc = markdown`
common use-case: vary Blur based on the position
`;
export const descAfter = markdown`
We use a texture to map the depth of the Blur.
`;
export const toolbox = [
{ prop: "factor",
title: "Blur",
Editor: makeFloatSlider(0, 8, 0.2) },
{ prop: "passes",
title: value => `Blur Passes (${value})`,
Editor: makeFloatSlider(1, 8, 1) },
{ prop: "map",
title: "Blur Texture Map",
Editor: StaticBlurMap },
];

View File

@ -0,0 +1,46 @@
//@flow
import React, { Component } from "react";
import { Shaders, Node, Bus, GLSL } from "gl-react";
import { Surface } from "gl-react-dom";
import {BlurV} from "../blurmap";
import timeLoop from "../../HOC/timeLoop";
const shaders = Shaders.create({
ConicalGradiant: {
frag: GLSL`precision highp float;
varying vec2 uv;
uniform float phase;
const float PI = 3.14159;
void main () {
gl_FragColor = vec4(vec3(
mod(phase + atan(uv.x-0.5, uv.y-0.5)/(2.0*PI), 1.0)
), 1.0);
}` }
});
const ConicalGradiantLoop = timeLoop(({ time }) =>
<Node
shader={shaders.ConicalGradiant}
uniforms={{ phase: time/3000 }}
/>);
export default class Example extends Component {
render() {
const { factor, passes } = this.props;
// <ConicalGradiant/> also needs to be computed once.
return (
<Surface width={600} height={284}>
<Bus ref="blurMapBus">
<ConicalGradiantLoop />
</Bus>
<BlurV map={() => this.refs.blurMapBus} passes={passes} factor={factor}>
https://i.imgur.com/NjbLHx2.jpg
</BlurV>
</Surface>
);
}
static defaultProps = {
factor: 6,
passes: 4,
};
};

View File

@ -0,0 +1,14 @@
import markdown from "../../markdown";
import makeFloatSlider from "../../toolbox/makeFloatSlider";
export const title = "Blur with intensity map & multi-pass";
export const desc = markdown`
Any arbitrary shader can be used as a blur map!
`;
export const toolbox = [
{ prop: "factor",
title: "Blur",
Editor: makeFloatSlider(0, 8, 0.2) },
{ prop: "passes",
title: value => `Blur Passes (${value})`,
Editor: makeFloatSlider(1, 8, 1) },
];

View File

@ -0,0 +1,62 @@
//@flow
import React, { Component } from "react";
import { Shaders, Node, Bus, GLSL } from "gl-react";
import { Surface } from "gl-react-dom";
import {BlurV} from "../blurmap";
const shaders = Shaders.create({
Offset: {
frag: GLSL`precision highp float;
varying vec2 uv;
uniform sampler2D t;
uniform vec2 offset;
void main () {
gl_FragColor = texture2D(t, uv + offset);
}`
}
});
const Offset = ({ t, offset }) =>
<Node shader={shaders.Offset} uniforms={{ t, offset }} />;
export default class Example extends Component {
state = {
offset: [0,0],
};
render() {
const { map } = this.props;
const { offset } = this.state;
// Sharing computation of a GL Node.
// <Offset /> should not be passed straight to BlurV's map because
// it would duplicates it in the tree ([passes] times)
// Instead, we need to express a graph and share the
// computation with a Bus ref.
// We pass to BlurV's map prop a function that resolve that ref.
return (
<Surface
width={600}
height={284}
onMouseMove={this.onMouseMove}
onMouseLeave={this.onMouseLeave}>
<Bus ref="blurMapBus">
<Offset offset={offset} t={map} />
</Bus>
<BlurV map={()=>this.refs.blurMapBus} passes={6} factor={6}>
https://i.imgur.com/NjbLHx2.jpg
</BlurV>
</Surface>
);
}
onMouseMove = (e: *) => {
const rect = e.target.getBoundingClientRect();
this.setState({ offset: [
-(e.clientX - rect.left - rect.width/2) / rect.width,
(e.clientY - rect.top - rect.height/2) / rect.height
] });
};
onMouseLeave = () => this.setState({ offset: [0,0] });
static defaultProps = {
map: StaticBlurMap.images[0],
};
};
import StaticBlurMap from "../../toolbox/StaticBlurMap";

View File

@ -0,0 +1,16 @@
import markdown from "../../markdown";
import StaticBlurMap from "../../toolbox/StaticBlurMap";
export const title = "Blur map and Mouse position";
export const desc = markdown`
Dynamically change Blur Map with mouse move
`;
export const descAfter = markdown`
This example is the first to show the need of Sharing Computation:
We want the "offset" framebuffer to be computed once,
we use gl-react [\`<Bus>\`](/api#bus) concept for this.
`;
export const toolbox = [
{ prop: "map",
title: "Blur Texture Map",
Editor: StaticBlurMap },
];

View File

@ -0,0 +1,45 @@
//@flow
import React, { Component } from "react";
import { connectSize } from "gl-react";
import { Surface } from "gl-react-dom";
import { Blur1D } from "../blurxy";
// empirical strategy to chose a 2d vector for a blur pass
const NORM = Math.sqrt(2)/2;
export const directionForPass = (p: number, factor: number, total: number) => {
const f = factor * 2 * Math.ceil(p / 2) / total;
switch ((p-1) % 4) { // alternate horizontal, vertical and 2 diagonals
case 0: return [f,0];
case 1: return [0,f];
case 2: return [f*NORM,f*NORM];
default: return [f*NORM,-f*NORM];
}
}
// recursively apply Blur1D to make a multi pass Blur component
export const Blur = connectSize(({ children, factor, passes }) => {
const rec = pass =>
pass <= 0
? children
: <Blur1D direction={directionForPass(pass, factor, passes)}>
{rec(pass-1)}
</Blur1D>;
return rec(passes);
});
export default class Example extends Component {
render() {
const { factor, passes } = this.props;
return (
<Surface width={400} height={300}>
<Blur passes={passes} factor={factor}>
https://i.imgur.com/iPKTONG.jpg
</Blur>
</Surface>
);
}
static defaultProps = {
factor: 2,
passes: 4,
};
}

View File

@ -0,0 +1,22 @@
import markdown from "../../markdown";
import makeFloatSlider from "../../toolbox/makeFloatSlider";
export const title = "multi-pass Blur";
export const desc = markdown`
For a better Blur quality, we need more than 2 passes.
We generalize the concept to make a N-passes component.
4-passes Blur is good enough, even without downscaling.
`;
export const descAfter = markdown`
We apply blur on various direction, not just X and Y axis.
We also vary the intensity for each pass.
\`directionForPass\` implements this empirical approach.
`;
export const toolbox = [
{ prop: "factor",
title: "Blur",
Editor: makeFloatSlider(0, 8, 0.2) },
{ prop: "passes",
title: passes => `Blur Passes (${passes})`,
Editor: makeFloatSlider(0, 8, 1) },
];

View File

@ -0,0 +1,45 @@
//@flow
import React, { Component } from "react";
import { Bus } from "gl-react";
import { Surface } from "gl-react-dom";
import {BlurV} from "../blurmap";
import {Saturate} from "../saturation";
import {Video, videoMP4} from "../video";
// We must use a <Bus> if we don't want the <video> element to be duplicated
// per Blur pass.. Also since we can dynamically change the number of passes,
// it changes the tree level, (e.g. Blur1D>Blur1D>video becomes Blur1D>video)
// and React always destroys and recreates the instance during reconcialition.
export default class Example extends Component {
render() {
const { factor, passes, contrast, saturation, brightness, map } = this.props;
return (
<Surface width={480} height={360} pixelRatio={1}>
<Bus ref="vid">
<Saturate contrast={contrast} saturation={saturation} brightness={brightness}>
{ redraw =>
<Video onFrame={redraw} autoPlay loop>
<source type="video/mp4" src={videoMP4} />
</Video> }
</Saturate>
</Bus>
<BlurV map={map} passes={passes} factor={factor}>
{ // as a texture, we give a function that resolve the video ref
() => this.refs.vid
}
</BlurV>
</Surface>
);
}
static defaultProps = {
contrast: 1,
saturation: 1,
brightness: 1,
factor: 2,
passes: 4,
map: StaticBlurMap.images[0],
};
}
import StaticBlurMap from "../../toolbox/StaticBlurMap";

View File

@ -0,0 +1,24 @@
import makeFloatSlider from "../../toolbox/makeFloatSlider";
import StaticBlurMap from "../../toolbox/StaticBlurMap";
export const toolbox = [
{ prop: "contrast",
title: "Contrast",
Editor: makeFloatSlider(0, 2, 0.05) },
{ prop: "saturation",
title: "Saturation",
Editor: makeFloatSlider(0, 2, 0.05) },
{ prop: "brightness",
title: "Brightness",
Editor: makeFloatSlider(0, 2, 0.05) },
{ prop: "factor",
title: "Blur",
Editor: makeFloatSlider(0, 8, 0.2) },
{ prop: "passes",
title: value => `Blur Passes (${value})`,
Editor: makeFloatSlider(1, 8, 1) },
{ prop: "map",
title: "Blur Texture Map",
Editor: StaticBlurMap },
];
export const title = "Video + multi-pass Blur + contrast/saturation/brightness";

View File

@ -0,0 +1,61 @@
//@flow
import React, { Component } from "react";
import {Shaders, Node, GLSL, connectSize} from "gl-react";
import { Surface } from "gl-react-dom";
const shaders = Shaders.create({
blur1D: { // blur9: from https://github.com/Jam3/glsl-fast-gaussian-blur
frag: GLSL`
precision highp float;
varying vec2 uv;
uniform sampler2D t;
uniform vec2 direction, resolution;
vec4 blur9(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) {
vec4 color = vec4(0.0);
vec2 off1 = vec2(1.3846153846) * direction;
vec2 off2 = vec2(3.2307692308) * direction;
color += texture2D(image, uv) * 0.2270270270;
color += texture2D(image, uv + (off1 / resolution)) * 0.3162162162;
color += texture2D(image, uv - (off1 / resolution)) * 0.3162162162;
color += texture2D(image, uv + (off2 / resolution)) * 0.0702702703;
color += texture2D(image, uv - (off2 / resolution)) * 0.0702702703;
return color;
}
void main() {
gl_FragColor = blur9(t, uv, resolution, direction);
}` }
});
// This implements a blur on a single direction (x or y axis for instance)
// connectSize will inject for us the width/height from context if not provided
export const Blur1D =
connectSize(({ children: t, direction, width, height }) =>
<Node
shader={shaders.blur1D}
uniforms={{ t, resolution: [ width, height ], direction }}
/>);
// BlurXY is a basic blur that apply Blur1D on Y and then on X
export const BlurXY =
connectSize(({ factor, children }) =>
<Blur1D direction={[ factor, 0 ]}>
<Blur1D direction={[ 0, factor ]}>
{children}
</Blur1D>
</Blur1D>);
export default class Example extends Component {
render() {
const { factor } = this.props;
return (
<Surface width={400} height={300}>
<BlurXY factor={factor}>
https://i.imgur.com/iPKTONG.jpg
</BlurXY>
</Surface>
);
}
static defaultProps = {
factor: 1,
};
}

View File

@ -0,0 +1,22 @@
import markdown from "../../markdown";
import makeFloatSlider from "../../toolbox/makeFloatSlider";
export const title = "simple Blur (2-passes)";
export const desc = markdown`
Implementing Blur efficiently isn't trivial.
This approach do 2 passes *(X and Y)*
`;
export const descAfter = markdown`
We apply a linear blur (9 gaussian lookup)
on Y axis and then do the same on X axis.
We can see the blur quality isn't perfect,
[next example](/blurxydownscale) will improve that.
`;
export const toolbox = [
{ prop: "factor",
title: "Blur",
Editor: makeFloatSlider(0, 8, 0.2) },
];

Some files were not shown because too many files have changed in this diff Show More