├── .vscode
└── settings.json
├── .gitattributes
├── examples
├── esm-pixi
│ ├── assets
│ │ └── snake.png
│ ├── source
│ │ ├── entities
│ │ │ ├── terrain
│ │ │ │ ├── Rock.js
│ │ │ │ └── Snow.js
│ │ │ └── Snake.js
│ │ ├── keyboard.js
│ │ ├── World.js
│ │ └── main.js
│ ├── index.html
│ └── lib
│ │ ├── seedrandom.min.js
│ │ ├── simplex-noise.min.js
│ │ └── uuid.js
├── script-tag-coffee
│ ├── source
│ │ ├── entities
│ │ │ └── terrain
│ │ │ │ ├── Rock.coffee
│ │ │ │ └── Snow.coffee
│ │ ├── keyboard.coffee
│ │ ├── World.coffee
│ │ └── main.coffee
│ ├── index.html
│ └── lib
│ │ ├── seedrandom.min.js
│ │ ├── simplex-noise.min.js
│ │ └── uuid.js
├── webpack-coffee
│ ├── source
│ │ ├── entities
│ │ │ └── terrain
│ │ │ │ ├── Rock.coffee
│ │ │ │ └── Snow.coffee
│ │ ├── keyboard.coffee
│ │ ├── World.coffee
│ │ └── main.coffee
│ ├── package.json
│ ├── index.html
│ ├── lib
│ │ ├── seedrandom.min.js
│ │ ├── simplex-noise.min.js
│ │ └── uuid.js
│ └── webpack.config.js
└── esm
│ ├── source
│ ├── entities
│ │ └── terrain
│ │ │ ├── Rock.js
│ │ │ └── Snow.js
│ ├── keyboard.js
│ ├── World.js
│ └── main.js
│ ├── index.html
│ └── lib
│ ├── seedrandom.min.js
│ ├── simplex-noise.min.js
│ └── uuid.js
├── source
├── entity-class-registry.coffee
├── rename-object-key.coffee
├── index.coffee
├── structure
│ ├── Structure.coffee
│ ├── Pose.coffee
│ ├── PolygonStructure.coffee
│ └── BoneStructure.coffee
├── Mouse.coffee
├── jsMenus
│ ├── LICENSE.md
│ └── jsMenus.css
├── View.coffee
├── helpers.coffee
├── icons
│ ├── plus.svg
│ ├── smooth.svg
│ ├── select.svg
│ ├── roughen.svg
│ ├── push-arrows-in-circle.svg
│ ├── delete.svg
│ ├── brush.svg
│ ├── push-hand-no-arrow.svg
│ ├── push-hand-with-arrow.svg
│ └── sculpt.svg
├── components
│ ├── AnimBar.coffee
│ ├── ToolsBar.coffee
│ ├── EntitiesBar.coffee
│ ├── EntityPreview.coffee
│ ├── Anim.coffee
│ └── AnimGroup.coffee
├── base-entities
│ ├── Terrain.coffee
│ └── Entity.coffee
├── arcs-overlap.js
├── styles.css
└── arcs-overlap-tests.html
├── LICENSE.txt
├── .gitignore
├── webpack.config.js
├── package.json
├── cspell.json
└── TESTS.txt
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "workbench.colorTheme": "Solarized Light"
3 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/examples/esm-pixi/assets/snake.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1j01/skele2d/HEAD/examples/esm-pixi/assets/snake.png
--------------------------------------------------------------------------------
/source/entity-class-registry.coffee:
--------------------------------------------------------------------------------
1 |
2 | # TODO: replace this with just passing a list of entities to the Editor (and stuff), probably
3 |
4 | export entityClasses = {}
5 | export addEntityClass = (constructor)->
6 | entityClasses[constructor.name] = constructor
7 |
--------------------------------------------------------------------------------
/source/rename-object-key.coffee:
--------------------------------------------------------------------------------
1 |
2 | export default (object, old_key, new_key)->
3 | new_object = {}
4 | for k, v of object
5 | if k is old_key
6 | new_object[new_key] = v
7 | else
8 | new_object[k] = v
9 | # return new_object
10 | for k, v of object
11 | delete object[k]
12 | for k, v of new_object
13 | object[k] = v
14 |
--------------------------------------------------------------------------------
/examples/script-tag-coffee/source/entities/terrain/Rock.coffee:
--------------------------------------------------------------------------------
1 | class @Rock extends Terrain
2 | addEntityClass(@)
3 | constructor: ->
4 | super()
5 | @bbox_padding = 20
6 |
7 | draw: (ctx, view)->
8 | ctx.beginPath()
9 | for point_name, point of @structure.points
10 | ctx.lineTo(point.x, point.y)
11 | ctx.closePath()
12 | ctx.fillStyle = "#63625F"
13 | ctx.fill()
14 |
--------------------------------------------------------------------------------
/examples/script-tag-coffee/source/entities/terrain/Snow.coffee:
--------------------------------------------------------------------------------
1 | class @Snow extends Terrain
2 | addEntityClass(@)
3 | constructor: ->
4 | super()
5 | @bbox_padding = 20
6 |
7 | draw: (ctx, view)->
8 | ctx.beginPath()
9 | for point_name, point of @structure.points
10 | ctx.lineTo(point.x, point.y)
11 | ctx.closePath()
12 | ctx.fillStyle = "#fcfeff"
13 | ctx.fill()
14 |
--------------------------------------------------------------------------------
/examples/webpack-coffee/source/entities/terrain/Rock.coffee:
--------------------------------------------------------------------------------
1 | import {Terrain, addEntityClass} from "skele2d"
2 |
3 | export default class Rock extends Terrain
4 | addEntityClass(@)
5 | constructor: ->
6 | super()
7 | @bbox_padding = 20
8 |
9 | draw: (ctx, view)->
10 | ctx.beginPath()
11 | for point_name, point of @structure.points
12 | ctx.lineTo(point.x, point.y)
13 | ctx.closePath()
14 | ctx.fillStyle = "#63625F"
15 | ctx.fill()
16 |
--------------------------------------------------------------------------------
/examples/webpack-coffee/source/entities/terrain/Snow.coffee:
--------------------------------------------------------------------------------
1 | import {Terrain, addEntityClass} from "skele2d"
2 |
3 | export default class Snow extends Terrain
4 | addEntityClass(@)
5 | constructor: ->
6 | super()
7 | @bbox_padding = 20
8 |
9 | draw: (ctx, view)->
10 | ctx.beginPath()
11 | for point_name, point of @structure.points
12 | ctx.lineTo(point.x, point.y)
13 | ctx.closePath()
14 | ctx.fillStyle = "#fcfeff"
15 | ctx.fill()
16 |
--------------------------------------------------------------------------------
/examples/esm/source/entities/terrain/Rock.js:
--------------------------------------------------------------------------------
1 | import { Terrain, addEntityClass } from "skele2d";
2 |
3 | export default class Rock extends Terrain {
4 | constructor() {
5 | super();
6 | this.bbox_padding = 20;
7 | }
8 |
9 | draw(ctx, view) {
10 | ctx.beginPath();
11 | for (var point_name in this.structure.points) {
12 | var point = this.structure.points[point_name];
13 | ctx.lineTo(point.x, point.y);
14 | }
15 | ctx.closePath();
16 | ctx.fillStyle = "#63625F";
17 | ctx.fill();
18 | }
19 | };
20 | addEntityClass(Rock);
21 |
--------------------------------------------------------------------------------
/examples/esm/source/entities/terrain/Snow.js:
--------------------------------------------------------------------------------
1 | import { Terrain, addEntityClass } from "skele2d";
2 |
3 | export default class Snow extends Terrain {
4 | constructor() {
5 | super();
6 | this.bbox_padding = 20;
7 | }
8 |
9 | draw(ctx, view) {
10 | ctx.beginPath();
11 | for (var point_name in this.structure.points) {
12 | var point = this.structure.points[point_name];
13 | ctx.lineTo(point.x, point.y);
14 | }
15 | ctx.closePath();
16 | ctx.fillStyle = "#fcfeff";
17 | ctx.fill();
18 | }
19 | };
20 | addEntityClass(Snow);
21 |
--------------------------------------------------------------------------------
/examples/esm-pixi/source/entities/terrain/Rock.js:
--------------------------------------------------------------------------------
1 | import { Terrain, addEntityClass } from "skele2d";
2 |
3 | export default class Rock extends Terrain {
4 | constructor() {
5 | super();
6 | this.bbox_padding = 20;
7 | }
8 |
9 | draw(ctx, view) {
10 | ctx.beginPath();
11 | for (var point_name in this.structure.points) {
12 | var point = this.structure.points[point_name];
13 | ctx.lineTo(point.x, point.y);
14 | }
15 | ctx.closePath();
16 | ctx.fillStyle = "#63625F";
17 | ctx.fill();
18 | }
19 | };
20 | addEntityClass(Rock);
21 |
--------------------------------------------------------------------------------
/examples/esm-pixi/source/entities/terrain/Snow.js:
--------------------------------------------------------------------------------
1 | import { Terrain, addEntityClass } from "skele2d";
2 |
3 | export default class Snow extends Terrain {
4 | constructor() {
5 | super();
6 | this.bbox_padding = 20;
7 | }
8 |
9 | draw(ctx, view) {
10 | ctx.beginPath();
11 | for (var point_name in this.structure.points) {
12 | var point = this.structure.points[point_name];
13 | ctx.lineTo(point.x, point.y);
14 | }
15 | ctx.closePath();
16 | ctx.fillStyle = "#fcfeff";
17 | ctx.fill();
18 | }
19 | };
20 | addEntityClass(Snow);
21 |
--------------------------------------------------------------------------------
/source/index.coffee:
--------------------------------------------------------------------------------
1 | import Entity from "./base-entities/Entity.coffee"
2 | import Terrain from "./base-entities/Terrain.coffee"
3 | import Structure from "./structure/Structure.coffee"
4 |
5 | import BoneStructure from "./structure/BoneStructure.coffee"
6 | import PolygonStructure from "./structure/BoneStructure.coffee"
7 | import Pose from "./structure/Pose.coffee"
8 |
9 | import Editor from "./Editor.coffee"
10 | import View from "./View.coffee"
11 | import Mouse from "./Mouse.coffee"
12 |
13 | import {entityClasses, addEntityClass} from "./entity-class-registry.coffee"
14 |
15 | import * as helpers from "./helpers.coffee"
16 |
17 | export {
18 | Entity
19 | Terrain
20 | Structure
21 |
22 | BoneStructure
23 | PolygonStructure
24 | Pose
25 |
26 | Editor
27 | View
28 | Mouse
29 |
30 | entityClasses
31 | addEntityClass
32 |
33 | helpers
34 | }
35 |
--------------------------------------------------------------------------------
/examples/script-tag-coffee/source/keyboard.coffee:
--------------------------------------------------------------------------------
1 |
2 | specialKeys =
3 | backspace: 8, tab: 9, clear: 12
4 | enter: 13, return: 13
5 | esc: 27, escape: 27, space: 32
6 | left: 37, up: 38
7 | right: 39, down: 40
8 | del: 46, delete: 46
9 | home: 36, end: 35
10 | pageup: 33, pagedown: 34
11 | ',': 188, '.': 190, '/': 191
12 | '`': 192, '-': 189, '=': 187
13 | ';': 186, '\'': 222
14 | '[': 219, ']': 221, '\\': 220
15 |
16 | keyCodeFor = (keyName)->
17 | specialKeys[keyName.toLowerCase()] ? keyName.toUpperCase().charCodeAt(0)
18 |
19 | keys = {}
20 | prev_keys = {}
21 | addEventListener "keydown", (e)-> keys[e.keyCode] = true
22 | addEventListener "keyup", (e)-> delete keys[e.keyCode]
23 |
24 | @keyboard =
25 | wasJustPressed: (keyName)->
26 | keys[keyCodeFor(keyName)]? and not prev_keys[keyCodeFor(keyName)]?
27 | isHeld: (keyName)->
28 | keys[keyCodeFor(keyName)]?
29 | resetForNextStep: ->
30 | prev_keys = {}
31 | prev_keys[k] = v for k, v of keys
32 |
--------------------------------------------------------------------------------
/examples/webpack-coffee/source/keyboard.coffee:
--------------------------------------------------------------------------------
1 |
2 | specialKeys =
3 | backspace: 8, tab: 9, clear: 12
4 | enter: 13, return: 13
5 | esc: 27, escape: 27, space: 32
6 | left: 37, up: 38
7 | right: 39, down: 40
8 | del: 46, delete: 46
9 | home: 36, end: 35
10 | pageup: 33, pagedown: 34
11 | ',': 188, '.': 190, '/': 191
12 | '`': 192, '-': 189, '=': 187
13 | ';': 186, '\'': 222
14 | '[': 219, ']': 221, '\\': 220
15 |
16 | keyCodeFor = (keyName)->
17 | specialKeys[keyName.toLowerCase()] ? keyName.toUpperCase().charCodeAt(0)
18 |
19 | keys = {}
20 | prev_keys = {}
21 | addEventListener "keydown", (e)-> keys[e.keyCode] = true
22 | addEventListener "keyup", (e)-> delete keys[e.keyCode]
23 |
24 | keyboard =
25 | wasJustPressed: (keyName)->
26 | keys[keyCodeFor(keyName)]? and not prev_keys[keyCodeFor(keyName)]?
27 | isHeld: (keyName)->
28 | keys[keyCodeFor(keyName)]?
29 | resetForNextStep: ->
30 | prev_keys = {}
31 | prev_keys[k] = v for k, v of keys
32 |
33 | export default keyboard
34 |
--------------------------------------------------------------------------------
/examples/esm/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Skele2D Example — ES Module Usage
6 |
7 |
8 |
9 |
10 |
11 |
21 |
22 |
23 |
24 |
25 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/source/structure/Structure.coffee:
--------------------------------------------------------------------------------
1 | import Pose from "./Pose.coffee"
2 |
3 | export default class Structure
4 | constructor: ->
5 | @clear()
6 |
7 | clear: ->
8 | @points = {}
9 | @segments = {}
10 |
11 | toJSON: ->
12 | {points} = @
13 | segments = {}
14 | for segment_name, segment of @segments
15 | segments[segment_name] = {}
16 | for k, v of segment when k not in ["a", "b"]
17 | segments[segment_name][k] = v
18 | {points, segments}
19 |
20 | fromJSON: (def)->
21 | @points = def.points
22 | @segments = {}
23 | for segment_name, seg_def of def.segments
24 | segment = {}
25 | segment[k] = v for k, v of seg_def
26 | segment.a = @points[segment.from]
27 | segment.b = @points[segment.to]
28 | @segments[segment_name] = segment
29 |
30 | setPose: (pose)->
31 | for point_name, point of pose.points
32 | # console.log point_name, point, @points[point_name]
33 | @points[point_name].x = point.x
34 | @points[point_name].y = point.y
35 |
36 | getPose: ->
37 | new Pose(@)
38 |
--------------------------------------------------------------------------------
/examples/webpack-coffee/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "skele2d-example",
3 | "description": "A demonstration of Skele2D",
4 | "version": "0.0.0",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/1j01/skele2d.git"
8 | },
9 | "bugs": {
10 | "url": "https://github.com/1j01/skele2d/issues"
11 | },
12 | "license": "UNLICENSED",
13 | "private": true,
14 | "scripts": {
15 | "build": "webpack --mode=production",
16 | "watch": "webpack --watch --mode=development",
17 | "start": "webpack-dev-server --hot --mode=development",
18 | "start-nw": "nw"
19 | },
20 | "main": "index.html",
21 | "window": {
22 | "title": "Skele2D Example",
23 | "id": "skele2d-example"
24 | },
25 | "devDependencies": {
26 | "coffee-loader": "^4.0.0",
27 | "coffeescript": "^2.7.0",
28 | "nw-dev": "^3.0.1",
29 | "webpack": "^5.75.0",
30 | "webpack-cli": "^5.0.1",
31 | "webpack-dev-server": "^4.11.1"
32 | },
33 | "dependencies": {
34 | "nw": "^0.73.0-sdk"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/examples/esm-pixi/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Skele2D Example — ES Module Usage with PIXI.js Rendering
6 |
7 |
8 |
9 |
10 |
11 |
21 |
22 |
23 |
24 |
25 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright © 2023 Isaiah Odhner
3 |
4 | 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:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | 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.
9 |
10 |
--------------------------------------------------------------------------------
/examples/webpack-coffee/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Skele2D Example — Webpack Usage
6 |
7 |
8 |
9 |
10 |
11 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/examples/esm/source/keyboard.js:
--------------------------------------------------------------------------------
1 | const specialKeys = {
2 | backspace: 8, tab: 9, clear: 12,
3 | enter: 13, return: 13,
4 | esc: 27, escape: 27, space: 32,
5 | left: 37, up: 38,
6 | right: 39, down: 40,
7 | del: 46, delete: 46,
8 | home: 36, end: 35,
9 | pageup: 33, pagedown: 34,
10 | ',': 188, '.': 190, '/': 191,
11 | '`': 192, '-': 189, '=': 187,
12 | ';': 186, '\'': 222,
13 | '[': 219, ']': 221, '\\': 220
14 | };
15 |
16 | const keyCodeFor = function (keyName) {
17 | return specialKeys[keyName.toLowerCase()] ?? keyName.toUpperCase().charCodeAt(0);
18 | };
19 |
20 | const keys = {};
21 | let prev_keys = {};
22 | addEventListener("keydown", e => keys[e.keyCode] = true);
23 | addEventListener("keyup", e => delete keys[e.keyCode]);
24 |
25 | const keyboard = {
26 | wasJustPressed(keyName) {
27 | return keys[keyCodeFor(keyName)] && !prev_keys[keyCodeFor(keyName)];
28 | },
29 | isHeld(keyName) {
30 | return keys[keyCodeFor(keyName)];
31 | },
32 | resetForNextStep() {
33 | prev_keys = {};
34 | for (var k in keys) {
35 | var v = keys[k];
36 | prev_keys[k] = v;
37 | }
38 | }
39 | };
40 |
41 | export default keyboard;
42 |
--------------------------------------------------------------------------------
/examples/esm-pixi/source/keyboard.js:
--------------------------------------------------------------------------------
1 | const specialKeys = {
2 | backspace: 8, tab: 9, clear: 12,
3 | enter: 13, return: 13,
4 | esc: 27, escape: 27, space: 32,
5 | left: 37, up: 38,
6 | right: 39, down: 40,
7 | del: 46, delete: 46,
8 | home: 36, end: 35,
9 | pageup: 33, pagedown: 34,
10 | ',': 188, '.': 190, '/': 191,
11 | '`': 192, '-': 189, '=': 187,
12 | ';': 186, '\'': 222,
13 | '[': 219, ']': 221, '\\': 220
14 | };
15 |
16 | const keyCodeFor = function (keyName) {
17 | return specialKeys[keyName.toLowerCase()] ?? keyName.toUpperCase().charCodeAt(0);
18 | };
19 |
20 | const keys = {};
21 | let prev_keys = {};
22 | addEventListener("keydown", e => keys[e.keyCode] = true);
23 | addEventListener("keyup", e => delete keys[e.keyCode]);
24 |
25 | const keyboard = {
26 | wasJustPressed(keyName) {
27 | return keys[keyCodeFor(keyName)] && !prev_keys[keyCodeFor(keyName)];
28 | },
29 | isHeld(keyName) {
30 | return keys[keyCodeFor(keyName)];
31 | },
32 | resetForNextStep() {
33 | prev_keys = {};
34 | for (var k in keys) {
35 | var v = keys[k];
36 | prev_keys[k] = v;
37 | }
38 | }
39 | };
40 |
41 | export default keyboard;
42 |
--------------------------------------------------------------------------------
/source/Mouse.coffee:
--------------------------------------------------------------------------------
1 |
2 | export default class Mouse
3 | constructor: (canvas)->
4 | @x = -Infinity
5 | @y = -Infinity
6 | @LMB = {down: no, pressed: no, released: no}
7 | @MMB = {down: no, pressed: no, released: no}
8 | @RMB = {down: no, pressed: no, released: no}
9 | @double_clicked = no
10 |
11 | # TODO: maybe have an init / initListeners / addListeners method?
12 | # doesn't seem good to add listeners in a constructor
13 | addEventListener "mousemove", (e)=>
14 | @x = e.clientX
15 | @y = e.clientY
16 |
17 | canvas.addEventListener "mousedown", (e)=>
18 | MB = @["#{"LMR"[e.button]}MB"]
19 | MB.down = yes
20 | MB.pressed = yes
21 |
22 | addEventListener "mouseup", (e)=>
23 | MB = @["#{"LMR"[e.button]}MB"]
24 | MB.down = no
25 | MB.released = yes
26 |
27 | canvas.addEventListener "dblclick", (e)=>
28 | MB = @["#{"LMR"[e.button]}MB"]
29 | MB.pressed = yes
30 | @double_clicked = yes
31 |
32 | resetForNextStep: ->
33 | @LMB.pressed = no
34 | @MMB.pressed = no
35 | @RMB.pressed = no
36 | @LMB.released = no
37 | @MMB.released = no
38 | @RMB.released = no
39 | @double_clicked = no
40 |
--------------------------------------------------------------------------------
/source/jsMenus/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Sam Wray
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | dist/
3 | build/
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 |
24 | # nyc test coverage
25 | .nyc_output
26 |
27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
28 | .grunt
29 |
30 | # Bower dependency directory (https://bower.io/)
31 | bower_components
32 |
33 | # node-waf configuration
34 | .lock-wscript
35 |
36 | # Compiled binary addons (https://nodejs.org/api/addons.html)
37 | build/Release
38 |
39 | # Dependency directories
40 | node_modules/
41 | jspm_packages/
42 |
43 | # Typescript v1 declaration files
44 | typings/
45 |
46 | # Optional npm cache directory
47 | .npm
48 |
49 | # Optional eslint cache
50 | .eslintcache
51 |
52 | # Optional REPL history
53 | .node_repl_history
54 |
55 | # Output of 'npm pack'
56 | *.tgz
57 |
58 | # Yarn Integrity file
59 | .yarn-integrity
60 |
61 | # dotenv environment variables file
62 | .env
63 |
64 |
--------------------------------------------------------------------------------
/source/View.coffee:
--------------------------------------------------------------------------------
1 |
2 | export default class View
3 | constructor: ->
4 | @center_x = 0
5 | @center_y = 0
6 | @scale = 1
7 | @width = 1
8 | @height = 1
9 |
10 | easeTowards: (to_view, smoothness)->
11 | @center_x += (to_view.center_x - @center_x) / (1 + smoothness / to_view.scale * @scale)
12 | @center_y += (to_view.center_y - @center_y) / (1 + smoothness / to_view.scale * @scale)
13 | @scale += (to_view.scale - @scale) / (1 + smoothness)
14 |
15 | testRect: (x, y, width, height, padding=0)->
16 | @center_x - @width / 2 / @scale - padding <= x <= @center_x + @width / 2 / @scale + padding and
17 | @center_y - @height / 2 / @scale - padding <= y <= @center_y + @height / 2 / @scale + padding
18 |
19 | toWorld: (point)->
20 | # x: (point.x + @center_x - @width / 2) / @scale
21 | # y: (point.y + @center_y - @height / 2) / @scale
22 | x: (point.x - @width / 2) / @scale + @center_x
23 | y: (point.y - @height / 2) / @scale + @center_y
24 |
25 | fromWorld: (point)->
26 | # x: point.x * @scale + @center_x + @width / 2
27 | # y: point.y * @scale + @center_y + @height / 2
28 | x: (point.x - @center_x) * @scale + @width / 2
29 | y: (point.y - @center_y) * @scale + @height / 2
30 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const makeConfig = ({ minimize, esm }) => {
4 | return {
5 | context: path.join(__dirname, 'source'),
6 | entry: [
7 | './index.coffee',
8 | ],
9 | experiments: {
10 | outputModule: esm,
11 | },
12 | output: {
13 | path: path.join(__dirname, 'dist'),
14 | filename: `skele2d${esm ? '.esm' : ''}${minimize ? '.min' : ''}.js`,
15 | library: esm ? {
16 | type: 'module',
17 | } : "skele2d",
18 | libraryTarget: esm ? undefined : 'umd',
19 | hashFunction: 'xxhash64',
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.coffee$/,
25 | use: ['coffee-loader'],
26 | },
27 | {
28 | test: /\.css$/,
29 | use: ['style-loader', 'css-loader']
30 | },
31 | {
32 | test: /\.(svg)$/i,
33 | type: "asset",
34 | },
35 | ],
36 | },
37 | optimization: {
38 | minimize,
39 | },
40 | };
41 | };
42 |
43 | const configs = [];
44 | for (const minimize of [false, true]) {
45 | for (const esm of [false, true]) {
46 | configs.push(makeConfig({ minimize, esm }));
47 | }
48 | }
49 | module.exports = configs;
50 |
--------------------------------------------------------------------------------
/examples/script-tag-coffee/source/World.coffee:
--------------------------------------------------------------------------------
1 | class @World
2 | constructor: ->
3 | @entities = []
4 |
5 | fromJSON: (def)->
6 | unless def.entities instanceof Array
7 | throw new Error "Expected entities to be an array, got #{def.entities}"
8 | @entities = (Entity.fromJSON(ent_def) for ent_def in def.entities)
9 | for entity in @entities
10 | entity.resolveReferences(@)
11 |
12 | getEntityByID: (id)->
13 | for entity in @entities
14 | return entity if entity.id is id
15 |
16 | getEntitiesOfType: (Class)->
17 | entity for entity in @entities when entity instanceof Class
18 |
19 | drawBackground: (ctx, view)->
20 | ctx.fillStyle = "#32C8FF"
21 | ctx.fillRect(0, 0, view.width, view.height)
22 |
23 | draw: (ctx, view)->
24 | # ctx.fillStyle = "#32C8FF"
25 | # {x, y} = view.toWorld({x: 0, y: 0})
26 | # {x: width, y: height} = view.toWorld({x: view.width, y: view.height})
27 | # ctx.fillRect(x, y, width-x, height-y)
28 | for entity in @entities
29 | ctx.save()
30 | ctx.translate(entity.x, entity.y)
31 | entity.draw(ctx, view)
32 | ctx.restore()
33 |
34 | collision: (point)->
35 | for entity in @entities when entity instanceof Terrain
36 | if entity.structure.pointInPolygon(entity.fromWorld(point))
37 | return true
38 | no
39 |
--------------------------------------------------------------------------------
/examples/webpack-coffee/source/World.coffee:
--------------------------------------------------------------------------------
1 | import {Terrain, Entity} from "skele2d"
2 |
3 | export default class World
4 | constructor: ->
5 | @entities = []
6 |
7 | fromJSON: (def)->
8 | unless def.entities instanceof Array
9 | throw new Error "Expected entities to be an array, got #{def.entities}"
10 | @entities = (Entity.fromJSON(ent_def) for ent_def in def.entities)
11 | for entity in @entities
12 | entity.resolveReferences(@)
13 |
14 | getEntityByID: (id)->
15 | for entity in @entities
16 | return entity if entity.id is id
17 |
18 | getEntitiesOfType: (Class)->
19 | entity for entity in @entities when entity instanceof Class
20 |
21 | drawBackground: (ctx, view)->
22 | ctx.fillStyle = "#32C8FF"
23 | ctx.fillRect(0, 0, view.width, view.height)
24 |
25 | draw: (ctx, view)->
26 | # ctx.fillStyle = "#32C8FF"
27 | # {x, y} = view.toWorld({x: 0, y: 0})
28 | # {x: width, y: height} = view.toWorld({x: view.width, y: view.height})
29 | # ctx.fillRect(x, y, width-x, height-y)
30 | for entity in @entities
31 | ctx.save()
32 | ctx.translate(entity.x, entity.y)
33 | entity.draw(ctx, view)
34 | ctx.restore()
35 |
36 | collision: (point)->
37 | for entity in @entities when entity instanceof Terrain
38 | if entity.structure.pointInPolygon(entity.fromWorld(point))
39 | return true
40 | no
41 |
--------------------------------------------------------------------------------
/source/structure/Pose.coffee:
--------------------------------------------------------------------------------
1 |
2 | import {lerpPoints} from "../helpers.coffee"
3 |
4 | export default class Pose
5 | constructor: (def)->
6 | @points = {}
7 | # if points?
8 | # {points} = points if points.points
9 | if def?
10 | {points} = def
11 | for point_name, {x, y} of points
12 | @points[point_name] = {x, y}
13 |
14 | @lerp: (a, b, b_ness)->
15 | # NOTE: no checks for matching sets of points
16 | result = new Pose
17 | for point_name, point_a of a.points
18 | point_b = b.points[point_name]
19 | result.points[point_name] = lerpPoints(point_a, point_b, b_ness)
20 | result
21 |
22 | @lerpAnimationLoop: (frames, soft_index)->
23 | a = frames[(~~(soft_index) + 0) %% frames.length]
24 | b = frames[(~~(soft_index) + 1) %% frames.length]
25 | Pose.lerp(a, b, soft_index %% 1)
26 |
27 | @alterPoints: (pose, fn)->
28 | result = new Pose
29 | for point_name, point of pose.points
30 | new_point = fn(point)
31 | new_point[k] ?= v for k, v of point
32 | result.points[point_name] = new_point
33 | result
34 |
35 | @copy: (pose)->
36 | Pose.alterPoints(pose, (-> {}))
37 |
38 | @horizontallyFlip: (pose, center_x=0)->
39 | Pose.alterPoints pose, (point)->
40 | x: center_x - point.x
41 | y: point.y
42 |
43 | @verticallyFlip: (pose, center_y=0)->
44 | Pose.alterPoints pose, (point)->
45 | x: point.x
46 | y: center_y - point.y
47 |
--------------------------------------------------------------------------------
/examples/script-tag-coffee/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Skele2D Example — Script Tag Usage
6 |
7 |
8 |
9 |
10 |
11 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/source/helpers.coffee:
--------------------------------------------------------------------------------
1 |
2 | distanceSquared = (v, w)-> (v.x - w.x) ** 2 + (v.y - w.y) ** 2
3 | export distance = (v, w)-> Math.sqrt(distanceSquared(v, w))
4 |
5 | distanceToLineSegmentSquared = (p, v, w)->
6 | l2 = distanceSquared(v, w)
7 | return distanceSquared(p, v) if l2 is 0
8 | t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2
9 | t = Math.max(0, Math.min(1, t))
10 | distanceSquared(p, {
11 | x: v.x + t * (w.x - v.x)
12 | y: v.y + t * (w.y - v.y)
13 | })
14 | export distanceToLineSegment = (p, v, w)->
15 | Math.sqrt(distanceToLineSegmentSquared(p, v, w))
16 |
17 | export lineSegmentsIntersect = (x1, y1, x2, y2, x3, y3, x4, y4)->
18 | a_dx = x2 - x1
19 | a_dy = y2 - y1
20 | b_dx = x4 - x3
21 | b_dy = y4 - y3
22 | s = (-a_dy * (x1 - x3) + a_dx * (y1 - y3)) / (-b_dx * a_dy + a_dx * b_dy)
23 | t = (+b_dx * (y1 - y3) - b_dy * (x1 - x3)) / (-b_dx * a_dy + a_dx * b_dy)
24 | (0 <= s <= 1 and 0 <= t <= 1)
25 |
26 | export closestPointOnLineSegment = (point, a, b)->
27 | # https://stackoverflow.com/a/3122532/2624876
28 | a_to_p = {x: point.x - a.x, y: point.y - a.y}
29 | a_to_b = {x: b.x - a.x, y: b.y - a.y}
30 | atb2 = a_to_b.x**2 + a_to_b.y**2
31 | atp_dot_atb = a_to_p.x*a_to_b.x + a_to_p.y*a_to_b.y
32 | t = atp_dot_atb / atb2
33 | return {x: a.x + a_to_b.x*t, y: a.y + a_to_b.y*t}
34 |
35 | export lerpPoints = (a, b, b_ness)->
36 | result = {}
37 | for k, v of a
38 | if typeof v is "number"
39 | result[k] = v + (b[k] - v) * b_ness
40 | else
41 | result[k] = v
42 | result
43 |
--------------------------------------------------------------------------------
/examples/esm/source/World.js:
--------------------------------------------------------------------------------
1 | import { Terrain, Entity } from "skele2d";
2 |
3 | export default class World {
4 | constructor() {
5 | this.entities = [];
6 | }
7 |
8 | fromJSON(def) {
9 | if (!(def.entities instanceof Array)) {
10 | throw new Error(`Expected entities to be an array, got ${def.entities}`);
11 | }
12 | this.entities = def.entities.map((ent_def) => Entity.fromJSON(ent_def));
13 | for (const entity of this.entities) {
14 | entity.resolveReferences(this);
15 | }
16 | }
17 |
18 | getEntityByID(id) {
19 | for (var entity of this.entities) {
20 | if (entity.id === id) { return entity; }
21 | }
22 | }
23 |
24 | getEntitiesOfType(Class) {
25 | return this.entities.filter((entity) => entity instanceof Class);
26 | }
27 |
28 | drawBackground(ctx, view) {
29 | ctx.fillStyle = "#32C8FF";
30 | ctx.fillRect(0, 0, view.width, view.height);
31 | }
32 |
33 | draw(ctx, view) {
34 | // ctx.fillStyle = "#32C8FF"
35 | // {x, y} = view.toWorld({x: 0, y: 0})
36 | // {x: width, y: height} = view.toWorld({x: view.width, y: view.height})
37 | // ctx.fillRect(x, y, width-x, height-y)
38 | for (var entity of this.entities) {
39 | ctx.save();
40 | ctx.translate(entity.x, entity.y);
41 | entity.draw(ctx, view);
42 | ctx.restore();
43 | }
44 | }
45 |
46 | collision(point) {
47 | for (var entity of this.entities) {
48 | if (entity instanceof Terrain) {
49 | if (entity.structure.pointInPolygon(entity.fromWorld(point))) {
50 | return true;
51 | }
52 | }
53 | }
54 | return false;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/examples/esm/lib/seedrandom.min.js:
--------------------------------------------------------------------------------
1 | !function(a,b){function c(c,j,k){var n=[];j=1==j?{entropy:!0}:j||{};var s=g(f(j.entropy?[c,i(a)]:null==c?h():c,3),n),t=new d(n),u=function(){for(var a=t.g(m),b=p,c=0;q>a;)a=(a+c)*l,b*=l,c=t.g(1);for(;a>=r;)a/=2,b/=2,c>>>=1;return(a+c)/b};return u.int32=function(){return 0|t.g(4)},u.quick=function(){return t.g(4)/4294967296},u["double"]=u,g(i(t.S),a),(j.pass||k||function(a,c,d,f){return f&&(f.S&&e(f,t),a.state=function(){return e(t,{})}),d?(b[o]=a,c):a})(u,s,"global"in j?j.global:this==b,j.state)}function d(a){var b,c=a.length,d=this,e=0,f=d.i=d.j=0,g=d.S=[];for(c||(a=[c++]);l>e;)g[e]=e++;for(e=0;l>e;e++)g[e]=g[f=s&f+a[e%c]+(b=g[e])],g[f]=b;(d.g=function(a){for(var b,c=0,e=d.i,f=d.j,g=d.S;a--;)b=g[e=s&e+1],c=c*l+g[s&(g[e]=g[f=s&f+b])+(g[f]=b)];return d.i=e,d.j=f,c})(l)}function e(a,b){return b.i=a.i,b.j=a.j,b.S=a.S.slice(),b}function f(a,b){var c,d=[],e=typeof a;if(b&&"object"==e)for(c in a)try{d.push(f(a[c],b-1))}catch(g){}return d.length?d:"string"==e?a:a+"\0"}function g(a,b){for(var c,d=a+"",e=0;ea;)a=(a+c)*l,b*=l,c=t.g(1);for(;a>=r;)a/=2,b/=2,c>>>=1;return(a+c)/b};return u.int32=function(){return 0|t.g(4)},u.quick=function(){return t.g(4)/4294967296},u["double"]=u,g(i(t.S),a),(j.pass||k||function(a,c,d,f){return f&&(f.S&&e(f,t),a.state=function(){return e(t,{})}),d?(b[o]=a,c):a})(u,s,"global"in j?j.global:this==b,j.state)}function d(a){var b,c=a.length,d=this,e=0,f=d.i=d.j=0,g=d.S=[];for(c||(a=[c++]);l>e;)g[e]=e++;for(e=0;l>e;e++)g[e]=g[f=s&f+a[e%c]+(b=g[e])],g[f]=b;(d.g=function(a){for(var b,c=0,e=d.i,f=d.j,g=d.S;a--;)b=g[e=s&e+1],c=c*l+g[s&(g[e]=g[f=s&f+b])+(g[f]=b)];return d.i=e,d.j=f,c})(l)}function e(a,b){return b.i=a.i,b.j=a.j,b.S=a.S.slice(),b}function f(a,b){var c,d=[],e=typeof a;if(b&&"object"==e)for(c in a)try{d.push(f(a[c],b-1))}catch(g){}return d.length?d:"string"==e?a:a+"\0"}function g(a,b){for(var c,d=a+"",e=0;ea;)a=(a+c)*l,b*=l,c=t.g(1);for(;a>=r;)a/=2,b/=2,c>>>=1;return(a+c)/b};return u.int32=function(){return 0|t.g(4)},u.quick=function(){return t.g(4)/4294967296},u["double"]=u,g(i(t.S),a),(j.pass||k||function(a,c,d,f){return f&&(f.S&&e(f,t),a.state=function(){return e(t,{})}),d?(b[o]=a,c):a})(u,s,"global"in j?j.global:this==b,j.state)}function d(a){var b,c=a.length,d=this,e=0,f=d.i=d.j=0,g=d.S=[];for(c||(a=[c++]);l>e;)g[e]=e++;for(e=0;l>e;e++)g[e]=g[f=s&f+a[e%c]+(b=g[e])],g[f]=b;(d.g=function(a){for(var b,c=0,e=d.i,f=d.j,g=d.S;a--;)b=g[e=s&e+1],c=c*l+g[s&(g[e]=g[f=s&f+b])+(g[f]=b)];return d.i=e,d.j=f,c})(l)}function e(a,b){return b.i=a.i,b.j=a.j,b.S=a.S.slice(),b}function f(a,b){var c,d=[],e=typeof a;if(b&&"object"==e)for(c in a)try{d.push(f(a[c],b-1))}catch(g){}return d.length?d:"string"==e?a:a+"\0"}function g(a,b){for(var c,d=a+"",e=0;ea;)a=(a+c)*l,b*=l,c=t.g(1);for(;a>=r;)a/=2,b/=2,c>>>=1;return(a+c)/b};return u.int32=function(){return 0|t.g(4)},u.quick=function(){return t.g(4)/4294967296},u["double"]=u,g(i(t.S),a),(j.pass||k||function(a,c,d,f){return f&&(f.S&&e(f,t),a.state=function(){return e(t,{})}),d?(b[o]=a,c):a})(u,s,"global"in j?j.global:this==b,j.state)}function d(a){var b,c=a.length,d=this,e=0,f=d.i=d.j=0,g=d.S=[];for(c||(a=[c++]);l>e;)g[e]=e++;for(e=0;l>e;e++)g[e]=g[f=s&f+a[e%c]+(b=g[e])],g[f]=b;(d.g=function(a){for(var b,c=0,e=d.i,f=d.j,g=d.S;a--;)b=g[e=s&e+1],c=c*l+g[s&(g[e]=g[f=s&f+b])+(g[f]=b)];return d.i=e,d.j=f,c})(l)}function e(a,b){return b.i=a.i,b.j=a.j,b.S=a.S.slice(),b}function f(a,b){var c,d=[],e=typeof a;if(b&&"object"==e)for(c in a)try{d.push(f(a[c],b-1))}catch(g){}return d.length?d:"string"==e?a:a+"\0"}function g(a,b){for(var c,d=a+"",e=0;e
2 |
3 |
4 |
47 |
--------------------------------------------------------------------------------
/source/icons/smooth.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
48 |
--------------------------------------------------------------------------------
/source/icons/select.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
48 |
--------------------------------------------------------------------------------
/source/icons/roughen.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
48 |
--------------------------------------------------------------------------------
/source/components/AnimBar.coffee:
--------------------------------------------------------------------------------
1 | import {Component} from "react"
2 | import E from "react-script"
3 | import {entityClasses} from "../entity-class-registry.coffee"
4 | import AnimGroup from "./AnimGroup.coffee"
5 |
6 | export default class AnimBar extends Component
7 | constructor: ->
8 | super()
9 | @state = {visible: no}
10 |
11 | render: ->
12 | {editor} = @props
13 | {visible, EntityClass} = @state
14 |
15 | entity = editor.editing_entity ? @shown_entity
16 | editing_an_animation = editor.editing_entity_animation_frame_index?
17 | @shown_entity = entity
18 |
19 | @anims = []
20 | # TODO: remove references from @anims on Anim::componentWillUnmount
21 | E ".bar.sidebar.anim-bar", class: {visible},
22 | E ".anims",
23 | E "h1", "Poses"
24 | E AnimGroup, {entity, EntityClass, array_to_push_anims_to: @anims, update: @update, editor, type_of_anims: "poses"}
25 | E "h1", "Animations"
26 | E AnimGroup, {entity, EntityClass, array_to_push_anims_to: @anims, update: @update, editor, type_of_anims: "animations"}
27 | E ".animation-frames", class: {visible: visible and editing_an_animation},
28 | E "h1", "Frames"
29 | E AnimGroup, {entity, EntityClass, array_to_push_anims_to: @anims, update: @update, editor, type_of_anims: "animation-frames", editing_frame_index: editor.editing_entity_animation_frame_index}
30 |
31 | update: (show)=>
32 | {editor} = @props
33 | {editing_entity_anim_name, editing_entity} = editor
34 |
35 | EntityClass = if editing_entity? then entityClasses[editing_entity._class_]
36 | show = show and EntityClass?.animations
37 | if show
38 | for anim in @anims
39 | pose = anim.props.get_pose()
40 | if pose?
41 | anim.entity_preview.entity.structure.setPose(pose)
42 | anim.entity_preview.update()
43 |
44 | @setState {visible: show, EntityClass, editing_entity_anim_name}
45 |
46 |
--------------------------------------------------------------------------------
/source/base-entities/Terrain.coffee:
--------------------------------------------------------------------------------
1 |
2 | import Entity from "./Entity.coffee"
3 | import PolygonStructure from "../structure/PolygonStructure.coffee"
4 | TAU = Math.PI * 2
5 |
6 | export default class Terrain extends Entity
7 | constructor: ->
8 | super()
9 | @structure = new PolygonStructure
10 | @simplex = new window.SimplexNoise?()
11 | @seed = Math.random()
12 |
13 | initLayout: ->
14 | radius = 30
15 | n_points = 15
16 | for theta in [TAU/n_points..TAU] by TAU/n_points
17 | point_x = Math.sin(theta) * radius
18 | point_y = Math.cos(theta) * radius
19 | non_squished_point_y_component = Math.max(point_y, -radius*0.5)
20 | point_y = non_squished_point_y_component + (point_y - non_squished_point_y_component) * 0.4
21 | # point_y = non_squished_point_y_component + pow(0.9, point_y - non_squished_point_y_component)
22 | # point_y = non_squished_point_y_component + pow(point_y - non_squished_point_y_component, 0.9)
23 | @structure.addVertex(point_x, point_y)
24 |
25 | toJSON: ->
26 | def = {}
27 | def[k] = v for k, v of @ when k isnt "simplex"
28 | def
29 |
30 | generate: ->
31 | @width = 5000
32 | @left = -2500
33 | @right = @left + @width
34 | @max_height = 400
35 | @bottom = 300
36 | res = 20
37 | @structure.clear()
38 | @structure.addVertex(@right, @bottom)
39 | @structure.addVertex(@left, @bottom)
40 | for x in [@left..@right] by res
41 | if @simplex
42 | noise =
43 | @simplex.noise2D(x / 2400, 0) +
44 | @simplex.noise2D(x / 500, 10) / 5 +
45 | @simplex.noise2D(x / 50, 30) / 100
46 | else
47 | # noise = Math.random() * 2 - 1
48 | # noise = Math.sin(x / 100) * 0.5 + 0.5
49 | noise = 0
50 | @structure.addVertex(x, @bottom - (noise + 1) / 2 * @max_height)
51 |
52 | draw: (ctx, view)->
53 | ctx.beginPath()
54 | for point_name, point of @structure.points
55 | ctx.lineTo(point.x, point.y)
56 | ctx.closePath()
57 | ctx.fillStyle = "#a5f"
58 | ctx.fill()
59 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "skele2d",
3 | "description": "A point-based game engine and editor",
4 | "version": "0.0.11",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/1j01/skele2d.git"
8 | },
9 | "bugs": {
10 | "url": "https://github.com/1j01/skele2d/issues"
11 | },
12 | "homepage": "https://github.com/1j01/skele2d#readme",
13 | "author": {
14 | "name": "Isaiah Odhner",
15 | "email": "isaiahodhner@gmail.com",
16 | "url": "https://isaiahodhner.io"
17 | },
18 | "keywords": [
19 | "skele2d",
20 | "game-engine",
21 | "engine",
22 | "point-based",
23 | "points",
24 | "skeleton",
25 | "skeletal",
26 | "bones",
27 | "structure",
28 | "polygons",
29 | "polygon",
30 | "animation",
31 | "pose",
32 | "canvas",
33 | "canvas-game",
34 | "game",
35 | "game-development",
36 | "2d",
37 | "editor",
38 | "game-editor",
39 | "level-editor",
40 | "world-editor",
41 | "game-world"
42 | ],
43 | "license": "MIT",
44 | "main": "dist/skele2d.js",
45 | "module": "dist/skele2d.esm.js",
46 | "files": [
47 | "dist"
48 | ],
49 | "directories": {
50 | "example": "examples"
51 | },
52 | "scripts": {
53 | "install-example": "npm link && cd examples/webpack-coffee && npm install && npm link skele2d",
54 | "example": "cd examples/webpack-coffee && npm start",
55 | "example-nw": "cd examples/webpack-coffee && npm run start-nw",
56 | "build": "webpack --mode=production",
57 | "watch": "webpack --watch --mode=development"
58 | },
59 | "devDependencies": {
60 | "coffee-loader": "^4.0.0",
61 | "coffeescript": "^2.7.0",
62 | "css-loader": "^6.7.3",
63 | "react": "^17.0.2",
64 | "react-dom": "^17.0.2",
65 | "react-script": "^2.0.5",
66 | "style-loader": "^3.3.1",
67 | "terser-webpack-plugin": "^5.3.6",
68 | "webpack": "^5.75.0",
69 | "webpack-cli": "^5.0.1"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/cspell.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "**",
4 | ".*/**"
5 | ],
6 | "ignorePaths": [
7 | ".git",
8 | ".gitignore",
9 | "node_modules",
10 | "package-lock.json",
11 | "lib/",
12 | "dist/",
13 | "build/",
14 | "*.log",
15 | "npm-debug.log"
16 | ],
17 | "enabledLanguageIds": [
18 | "asciidoc",
19 | "c",
20 | "cpp",
21 | "csharp",
22 | "css",
23 | "elixir",
24 | "erlang",
25 | "git-commit",
26 | "go",
27 | "graphql",
28 | "handlebars",
29 | "haskell",
30 | "html",
31 | "jade",
32 | "java",
33 | "javascript",
34 | "javascriptreact",
35 | "json",
36 | "jsonc",
37 | "jupyter",
38 | "latex",
39 | "less",
40 | "markdown",
41 | "php",
42 | "plaintext",
43 | "python",
44 | "pug",
45 | "restructuredtext",
46 | "rust",
47 | "scala",
48 | "scss",
49 | "scminput",
50 | "swift",
51 | "text",
52 | "typescript",
53 | "typescriptreact",
54 | "vue",
55 | "yaml",
56 | "yml",
57 | "coffeescript"
58 | ],
59 | "words": [
60 | "animatable",
61 | "anims",
62 | "Arimo",
63 | "bbox",
64 | "bezier",
65 | "callout",
66 | "checkmark",
67 | "contextmenu",
68 | "dblclick",
69 | "desynchronize",
70 | "ducktype",
71 | "EEXIST",
72 | "ENOENT",
73 | "grabbable",
74 | "haha",
75 | "hypot",
76 | "importmap",
77 | "initialisation",
78 | "instanceof",
79 | "isnt",
80 | "keydown",
81 | "keyup",
82 | "lerp",
83 | "mergely",
84 | "metrohash",
85 | "mousedown",
86 | "mousemove",
87 | "mouseup",
88 | "mousewheel",
89 | "npmignore",
90 | "nwjs",
91 | "Odhner",
92 | "offcenteredness",
93 | "onchange",
94 | "onmousemove",
95 | "onmouseup",
96 | "overridable",
97 | "pagedown",
98 | "pageup",
99 | "Pixi",
100 | "pmenu",
101 | "popdown",
102 | "rgba",
103 | "Roadmap",
104 | "scminput",
105 | "seedrandom",
106 | "sideless",
107 | "skele2d",
108 | "subbars",
109 | "Tiamblia",
110 | "tweening",
111 | "typeof",
112 | "uglifyjs",
113 | "uncompiled",
114 | "undoable",
115 | "undos",
116 | "unhighlight",
117 | "unminified",
118 | "Wray"
119 | ]
120 | }
121 |
--------------------------------------------------------------------------------
/source/icons/push-arrows-in-circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
61 |
--------------------------------------------------------------------------------
/source/icons/delete.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
53 |
--------------------------------------------------------------------------------
/source/components/ToolsBar.coffee:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import ReactDOM from "react-dom"
3 | import {Component} from "react"
4 | import E from "react-script"
5 | import selectIcon from "../icons/select.svg"
6 | import pushIcon from "../icons/push-arrows-in-circle.svg"
7 | import roughenIcon from "../icons/roughen.svg"
8 | import smoothIcon from "../icons/smooth.svg"
9 | import paintIcon from "../icons/brush.svg"
10 |
11 | import Terrain from "../base-entities/Terrain.coffee"
12 |
13 | export default class ToolsBar extends Component
14 | constructor: ->
15 | super()
16 | @state = {visible: no}
17 | @tools = [
18 | {name: "select", icon: selectIcon}
19 | {name: "sculpt", icon: pushIcon}
20 | {name: "roughen", icon: roughenIcon}
21 | {name: "smooth", icon: smoothIcon}
22 | {name: "paint", icon: paintIcon}
23 | ]
24 | for tool in @tools
25 | tool.buttonRef = React.createRef()
26 |
27 | render: ->
28 | {editor} = @props
29 | {visible} = @state
30 |
31 | {tool, brush_size} = editor
32 |
33 | E ".bar.tools-bar", class: {visible},
34 | E ".tools",
35 | @tools.map ({name, icon, buttonRef}, i)=>
36 | E "button.mdl-button.mdl-js-button.mdl-button--icon.mdl-button--colored",
37 | # E "button.mdl-button.mdl-js-button.mdl-button--colored",
38 | key: i
39 | ariaPressed: name is editor.tool
40 | ref: buttonRef
41 | disabled: name is "paint" and not (editor.editing_entity instanceof Terrain)
42 | onClick: (e)=>
43 | editor.tool = name
44 | editor.renderDOM()
45 | title: name[0].toUpperCase() + name.slice(1)
46 | # E "i.material-icons", E "i.material-symbols-outlined", icon
47 | E "img", src: icon
48 | E ".tool-options",
49 | E "label",
50 | E "span", "Brush Size:"
51 | E "input.mdl-slider.mdl-js-slider",
52 | type: "range", min: 0, max: 100, value: brush_size, tabIndex: 0
53 | disabled: tool not in ["sculpt", "roughen", "smooth", "paint"]
54 | style: minWidth: 200
55 | ref: (@brush_size_slider)=>
56 | onChange: (e)=>
57 | editor.brush_size = e.target.value
58 | editor.renderDOM()
59 |
60 | componentDidMount: ->
61 | componentHandler.upgradeElement(ReactDOM.findDOMNode(@brush_size_slider))
62 | for tool in @tools
63 | componentHandler.upgradeElement(ReactDOM.findDOMNode(tool.buttonRef.current))
64 |
65 | update: (show)=>
66 | {editor} = @props
67 | {editing_entity} = editor
68 |
69 | show = show and editing_entity
70 |
71 | @setState visible: show
72 |
--------------------------------------------------------------------------------
/source/components/EntitiesBar.coffee:
--------------------------------------------------------------------------------
1 | import {Component} from "react"
2 | import E from "react-script"
3 | import {distance} from "../helpers.coffee"
4 | import {entityClasses} from "../entity-class-registry.coffee"
5 | import EntityPreview from "./EntityPreview.coffee"
6 |
7 | export default class EntitiesBar extends Component
8 | constructor: ->
9 | super()
10 | @state = {visible: no}
11 | @cells = []
12 | @entity_previews = []
13 | for entity_class_name, EntityClass of entityClasses
14 | cell_name = entity_class_name.replace(/[a-z][A-Z]/g, (m)-> "#{m[0]} #{m[1]}")
15 | preview_error = null
16 | preview_entity = null
17 | try
18 | preview_entity = new EntityClass
19 | preview_entity.initLayout()
20 | catch error
21 | preview_error = error
22 | cell = {
23 | EntityClass
24 | name: cell_name
25 | preview_entity
26 | preview_error
27 | }
28 | @cells.push(cell)
29 |
30 | render: ->
31 | {editor} = @props
32 | {visible} = @state
33 | cell_preview_width = 200
34 | max_cell_preview_height = 100
35 | @entity_previews = []
36 | E ".bar.sidebar.entities-bar", class: {visible},
37 | for cell, i in @cells
38 | E "article.cell.grabbable",
39 | key: i
40 | onMouseDown: do (cell)=> (e)=>
41 | editor.selected_entities = []
42 | mouse_start = {x: e.clientX, y: e.clientY}
43 | addEventListener "mousemove", onmousemove = (e)=>
44 | if distance(mouse_start, {x: e.clientX, y: e.clientY}) > 4
45 | editor.undoable =>
46 | entity = new cell.EntityClass
47 | entity.initLayout()
48 | editor.world.entities.push(entity)
49 | editor.dragEntities([entity])
50 | removeEventListener "mousemove", onmousemove
51 | removeEventListener "mouseup", onmouseup
52 | addEventListener "mouseup", onmouseup = (e)=>
53 | removeEventListener "mousemove", onmousemove
54 | removeEventListener "mouseup", onmouseup
55 | E "h1.name", cell.name
56 | E EntityPreview,
57 | entity: cell.preview_entity
58 | max_width: cell_preview_width
59 | max_height: max_cell_preview_height
60 | preview_error: cell.preview_error
61 | ref: (ep)=>
62 | @entity_previews.push(ep) if ep?
63 |
64 | update: (show)=>
65 | {editor} = @props
66 |
67 | show = show and editor.dragging_entities.length is 0 and not editor.editing_entity
68 | if show isnt @state.visible
69 | @setState visible: show
70 |
71 | if show
72 | for entity_preview in @entity_previews
73 | entity_preview.update()
74 |
--------------------------------------------------------------------------------
/source/structure/PolygonStructure.coffee:
--------------------------------------------------------------------------------
1 | import Structure from "./Structure.coffee"
2 |
3 | export default class PolygonStructure extends Structure
4 | constructor: ->
5 | super() # calls @clear()
6 | # don't need to worry about calling onchange because can't be set at this point
7 | # but it is useful for the bounding box to be updated (via clear/signalChange/_update_bbox)
8 | # during construction.
9 |
10 | clear: ->
11 | super()
12 | @id_counter = 0
13 | @last_point_name = null
14 | @first_point_name = null
15 | @signalChange()
16 |
17 | signalChange: ->
18 | # API contract: bbox is updated before call to onchange
19 | @_update_bbox()
20 | @onchange?()
21 |
22 | toJSON: ->
23 | # Excluding segments, bbox_min/bbox_max, id_counter, first_point_name/last_point_name,
24 | # because they can all be derived from points.
25 | # (This class assumes the points/segments will not be renamed.)
26 | points: ({x, y} for point_name, {x, y} of @points)
27 |
28 | fromJSON: (def)->
29 | @points = {}
30 | @segments = {}
31 | @id_counter = 0
32 | @first_point_name = null
33 | @last_point_name = null
34 | for {x, y} in def.points
35 | @addVertex(x, y, false)
36 | @signalChange()
37 |
38 | addVertex: (x, y, registerChange=true)->
39 | from = @last_point_name
40 | name = ++@id_counter
41 | @first_point_name ?= name
42 | if @points[name]
43 | throw new Error "point/segment '#{name}' already exists adding vertex '#{name}'"
44 | @points[name] = {x, y, name}
45 | @last_point_name = name
46 | if @points[from]
47 | @segments[name] = {a: @points[from], b: @points[name]}
48 | @segments["closing"] = {a: @points[@last_point_name], b: @points[@first_point_name]}
49 | if registerChange
50 | @signalChange()
51 |
52 | _update_bbox: ->
53 | @bbox_min = {x: Infinity, y: Infinity}
54 | @bbox_max = {x: -Infinity, y: -Infinity}
55 | for point_name, point of @points
56 | @bbox_min.x = Math.min(@bbox_min.x, point.x)
57 | @bbox_min.y = Math.min(@bbox_min.y, point.y)
58 | @bbox_max.x = Math.max(@bbox_max.x, point.x)
59 | @bbox_max.y = Math.max(@bbox_max.y, point.y)
60 |
61 | pointInPolygon: ({x, y})->
62 | if x < @bbox_min.x or x > @bbox_max.x or y < @bbox_min.y or y > @bbox_max.y
63 | return false
64 |
65 | inside = no
66 | for segment_name, segment of @segments
67 | a_x = segment.a.x
68 | a_y = segment.a.y
69 | b_x = segment.b.x
70 | b_y = segment.b.y
71 | intersect =
72 | ((a_y > y) isnt (b_y > y)) and
73 | (x < (b_x - a_x) * (y - a_y) / (b_y - a_y) + a_x)
74 | inside = not inside if intersect
75 |
76 | inside
77 |
--------------------------------------------------------------------------------
/examples/script-tag-coffee/source/main.coffee:
--------------------------------------------------------------------------------
1 |
2 | Math.seedrandom("A world")
3 |
4 | world = new World
5 |
6 | terrain = new Snow
7 | world.entities.push terrain
8 | terrain.x = 0
9 | terrain.y = 0
10 | terrain.generate()
11 |
12 | canvas = document.createElement("canvas")
13 | document.body.appendChild(canvas)
14 | ctx = canvas.getContext("2d")
15 |
16 | view = new View
17 | view_to = new View
18 | view_smoothness = 7
19 | mouse = new Mouse(canvas)
20 |
21 | editor = new Editor(world, view, view_to, canvas, mouse)
22 | try
23 | editor.load()
24 | catch e
25 | console?.error? "Failed to load save:", e
26 |
27 | try
28 | view_to.center_x = view.center_x = parseFloat(localStorage.view_center_x) unless isNaN(localStorage.view_center_x)
29 | view_to.center_y = view.center_y = parseFloat(localStorage.view_center_y) unless isNaN(localStorage.view_center_y)
30 | view_to.scale = view.scale = parseFloat(localStorage.view_scale) unless isNaN(localStorage.view_scale)
31 |
32 | setInterval ->
33 | if editor.editing
34 | # TODO: should probably only save if you pan/zoom
35 | localStorage.view_center_x = view.center_x
36 | localStorage.view_center_y = view.center_y
37 | localStorage.view_scale = view_to.scale
38 | , 200
39 |
40 | do animate = ->
41 | return if window.CRASHED
42 | requestAnimationFrame(animate)
43 |
44 | canvas.width = innerWidth unless canvas.width is innerWidth
45 | canvas.height = innerHeight unless canvas.height is innerHeight
46 |
47 | ctx.clearRect(0, 0, canvas.width, canvas.height)
48 |
49 | if editor.editing and (editor.entities_bar.hovered_cell or ((editor.hovered_points.length or editor.hovered_entities.length) and not editor.selection_box))
50 | canvas.classList.add("grabbable")
51 | else
52 | canvas.classList.remove("grabbable")
53 |
54 | unless editor.editing
55 | for entity in world.entities # when entity isnt editor.editing_entity and entity not in editor.dragging_entities
56 | entity.step(world)
57 |
58 | # TODO: allow margin of offcenteredness
59 | # player = world.getEntitiesOfType(Player)[0]
60 | # view_to.center_x = player.x
61 | # view_to.center_y = player.y
62 |
63 | view.width = canvas.width
64 | view.height = canvas.height
65 |
66 | view.easeTowards(view_to, view_smoothness)
67 | editor.step() if editor.editing
68 | mouse.resetForNextStep()
69 |
70 | world.drawBackground(ctx, view)
71 | ctx.save()
72 | ctx.translate(canvas.width / 2, canvas.height / 2)
73 | ctx.scale(view.scale, view.scale)
74 | ctx.translate(-view.center_x, -view.center_y)
75 |
76 | world.draw(ctx, view)
77 | editor.draw(ctx, view) if editor.editing
78 |
79 | ctx.restore()
80 |
81 | editor.updateGUI()
82 |
83 | # keyboard.resetForNextStep()
84 |
--------------------------------------------------------------------------------
/source/components/EntityPreview.coffee:
--------------------------------------------------------------------------------
1 | import {Component} from "react"
2 | import E from "react-script"
3 | import Entity from "../base-entities/Entity.coffee"
4 | import View from "../View.coffee"
5 |
6 | export default class EntityPreview extends Component
7 | constructor: (props)->
8 | super()
9 | {entity, max_width, max_height} = props
10 | @state = {}
11 | try
12 | @entity = Entity.fromJSON(JSON.parse(JSON.stringify(entity)))
13 | @entity.facing_x = 1
14 | @view = new View
15 | entity_bbox = @entity.bbox()
16 | center_x = entity_bbox.x + entity_bbox.width / 2 - @entity.x
17 | center_y = entity_bbox.y + entity_bbox.height / 2 - @entity.y
18 | height = Math.min(entity_bbox.height, max_height)
19 | scale = height / entity_bbox.height
20 | catch error
21 | @state.preview_error = error
22 | @view = new View
23 | @view.width = max_width
24 | @view.height = if isFinite(height) then height else max_height
25 | @view.scale = if isFinite(scale) then scale else 1
26 | @view.center_x = center_x
27 | @view.center_y = center_y
28 | @view.is_preview = true
29 |
30 | render: ->
31 | # Props has priority over state for preview_error because errors during
32 | # construction of an entity are more important than errors during rendering.
33 | # An error during construction can easily lead to bogus errors during rendering.
34 | preview_error = @props.preview_error or @state.preview_error
35 | # Chrome includes the error message in the stack trace, but Firefox doesn't.
36 | if preview_error?.stack
37 | if preview_error.stack.includes(preview_error.toString())
38 | error_details = preview_error.stack
39 | else
40 | error_details = "#{preview_error.toString()}\n#{preview_error.stack}"
41 | else if preview_error
42 | error_details = preview_error
43 |
44 | E "div.entity-preview",
45 | E "canvas", ref: (@canvas)=>
46 | if preview_error?
47 | E "div.error", title: error_details, preview_error.toString()
48 |
49 | update: ->
50 | @canvas.width = @view.width
51 | @canvas.height = @view.height
52 | try
53 | entity_bbox = @entity.bbox()
54 | center_x = entity_bbox.x + entity_bbox.width / 2 - @entity.x
55 | center_y = entity_bbox.y + entity_bbox.height / 2 - @entity.y
56 | @view.center_x = center_x
57 | @view.center_y = center_y
58 |
59 | ctx = @canvas.getContext("2d")
60 | ctx.save()
61 | ctx.translate(@view.width/2, @view.height/2)
62 | ctx.scale(@view.scale, @view.scale)
63 | ctx.translate(-@view.center_x, -@view.center_y)
64 | @entity.draw(ctx, @view)
65 | ctx.restore()
66 | catch error
67 | # Earlier errors are generally more pertinent than later errors.
68 | @setState({preview_error: error}) unless @state.preview_error?
69 |
--------------------------------------------------------------------------------
/source/icons/brush.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
53 |
--------------------------------------------------------------------------------
/examples/esm-pixi/source/World.js:
--------------------------------------------------------------------------------
1 | import { Terrain, Entity } from "skele2d";
2 |
3 | export default class World {
4 | constructor() {
5 | this.entities = [];
6 | }
7 |
8 | fromJSON(def) {
9 | if (!(def.entities instanceof Array)) {
10 | throw new Error(`Expected entities to be an array, got ${def.entities}`);
11 | }
12 | this.entities = def.entities.map((ent_def) => Entity.fromJSON(ent_def));
13 | for (const entity of this.entities) {
14 | entity.resolveReferences(this);
15 | }
16 | }
17 |
18 | getEntityByID(id) {
19 | for (var entity of this.entities) {
20 | if (entity.id === id) { return entity; }
21 | }
22 | }
23 |
24 | getEntitiesOfType(Class) {
25 | return this.entities.filter((entity) => entity instanceof Class);
26 | }
27 |
28 | drawBackground(ctx, view) {
29 | // ctx.fillStyle = "#32C8FF";
30 | // ctx.fillRect(0, 0, view.width, view.height);
31 | }
32 |
33 | pixiUpdate(stage, ticker) {
34 | for (const entity of this.entities) {
35 | entity.pixiUpdate?.(stage, ticker);
36 | }
37 | }
38 |
39 | draw(ctx, view) {
40 | // ctx.fillStyle = "#32C8FF"
41 | // {x, y} = view.toWorld({x: 0, y: 0})
42 | // {x: width, y: height} = view.toWorld({x: view.width, y: view.height})
43 | // ctx.fillRect(x, y, width-x, height-y)
44 | for (var entity of this.entities) {
45 | ctx.save();
46 | ctx.translate(entity.x, entity.y);
47 | entity.draw(ctx, view);
48 | ctx.restore();
49 | }
50 | }
51 |
52 | collision(point, ...rest){
53 | // lineThickness doesn't apply to polygons like Terrain
54 | // also it's kind of a hack, because different entities could need different lineThicknesses
55 | // and different segments within an entity too
56 |
57 | let filter;
58 | const val = rest[0], obj = val != null ? val : {}, val1 = obj.types, types = val1 != null ? val1 : [Terrain], val2 = obj.lineThickness, lineThickness = val2 != null ? val2 : 5;
59 | if (typeof types === "function") {
60 | filter = types;
61 | } else {
62 | filter = entity=> types.some(type=> (entity instanceof type) && (entity.solid != null ? entity.solid : true));
63 | }
64 |
65 | for (let entity of Array.from(this.entities)) {
66 | if (filter(entity)) {
67 | const local_point = entity.fromWorld(point);
68 | if (entity.structure.pointInPolygon != null) {
69 | if (entity.structure.pointInPolygon(local_point)) {
70 | return entity;
71 | }
72 | } else {
73 | for (let segment_name in entity.structure.segments) {
74 | const segment = entity.structure.segments[segment_name];
75 | const dist = distanceToLineSegment(local_point, segment.a, segment.b);
76 | if (dist < lineThickness) {
77 | return entity;
78 | }
79 | }
80 | }
81 | }
82 | }
83 | return null;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/source/icons/push-hand-no-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
53 |
--------------------------------------------------------------------------------
/examples/webpack-coffee/source/main.coffee:
--------------------------------------------------------------------------------
1 |
2 | import {View, Mouse, Editor} from "skele2d"
3 | import World from "./World.coffee"
4 | import Rock from "./entities/terrain/Rock.coffee"
5 | import Snow from "./entities/terrain/Snow.coffee"
6 | # import keyboard from "./keyboard.coffee"
7 |
8 | Math.seedrandom("A world")
9 |
10 | world = new World
11 |
12 | terrain = new Snow
13 | world.entities.push terrain
14 | terrain.x = 0
15 | terrain.y = 0
16 | terrain.generate()
17 |
18 | canvas = document.createElement("canvas")
19 | document.body.appendChild(canvas)
20 | ctx = canvas.getContext("2d")
21 |
22 | view = new View
23 | view_to = new View
24 | view_smoothness = 7
25 | mouse = new Mouse(canvas)
26 |
27 | editor = new Editor(world, view, view_to, canvas, mouse)
28 | try
29 | editor.load()
30 | catch e
31 | console?.error? "Failed to load save:", e
32 |
33 | try
34 | view_to.center_x = view.center_x = parseFloat(localStorage.view_center_x) unless isNaN(localStorage.view_center_x)
35 | view_to.center_y = view.center_y = parseFloat(localStorage.view_center_y) unless isNaN(localStorage.view_center_y)
36 | view_to.scale = view.scale = parseFloat(localStorage.view_scale) unless isNaN(localStorage.view_scale)
37 |
38 | setInterval ->
39 | if editor.editing
40 | # TODO: should probably only save if you pan/zoom
41 | localStorage.view_center_x = view.center_x
42 | localStorage.view_center_y = view.center_y
43 | localStorage.view_scale = view_to.scale
44 | , 200
45 |
46 | do animate = ->
47 | return if window.CRASHED
48 | requestAnimationFrame(animate)
49 |
50 | canvas.width = innerWidth unless canvas.width is innerWidth
51 | canvas.height = innerHeight unless canvas.height is innerHeight
52 |
53 | ctx.clearRect(0, 0, canvas.width, canvas.height)
54 |
55 | if editor.editing and (editor.entities_bar.hovered_cell or ((editor.hovered_points.length or editor.hovered_entities.length) and not editor.selection_box))
56 | canvas.classList.add("grabbable")
57 | else
58 | canvas.classList.remove("grabbable")
59 |
60 | unless editor.editing
61 | for entity in world.entities # when entity isnt editor.editing_entity and entity not in editor.dragging_entities
62 | entity.step(world)
63 |
64 | # TODO: allow margin of offcenteredness
65 | # player = world.getEntitiesOfType(Player)[0]
66 | # view_to.center_x = player.x
67 | # view_to.center_y = player.y
68 |
69 | view.width = canvas.width
70 | view.height = canvas.height
71 |
72 | view.easeTowards(view_to, view_smoothness)
73 | editor.step() if editor.editing
74 | mouse.resetForNextStep()
75 |
76 | world.drawBackground(ctx, view)
77 | ctx.save()
78 | ctx.translate(canvas.width / 2, canvas.height / 2)
79 | ctx.scale(view.scale, view.scale)
80 | ctx.translate(-view.center_x, -view.center_y)
81 |
82 | world.draw(ctx, view)
83 | editor.draw(ctx, view) if editor.editing
84 |
85 | ctx.restore()
86 |
87 | editor.updateGUI()
88 |
89 | # keyboard.resetForNextStep()
90 |
--------------------------------------------------------------------------------
/examples/esm/source/main.js:
--------------------------------------------------------------------------------
1 | import { View, Mouse, Editor } from "skele2d";
2 | import World from "./World.js";
3 | import Rock from "./entities/terrain/Rock.js";
4 | import Snow from "./entities/terrain/Snow.js";
5 | // import keyboard from "./keyboard.js";
6 |
7 | Math.seedrandom("A world");
8 |
9 | const world = new World;
10 |
11 | const terrain = new Snow;
12 | world.entities.push(terrain);
13 | terrain.x = 0;
14 | terrain.y = 0;
15 | terrain.generate();
16 |
17 | const canvas = document.createElement("canvas");
18 | document.body.appendChild(canvas);
19 | const ctx = canvas.getContext("2d");
20 |
21 | const view = new View;
22 | const view_to = new View;
23 | const view_smoothness = 7;
24 | const mouse = new Mouse(canvas);
25 |
26 | const editor = new Editor(world, view, view_to, canvas, mouse);
27 | try {
28 | editor.load();
29 | } catch (e) {
30 | console.error?.("Failed to load save:", e);
31 | }
32 |
33 | try {
34 | if (!isNaN(localStorage.view_center_x)) { view_to.center_x = (view.center_x = parseFloat(localStorage.view_center_x)); }
35 | if (!isNaN(localStorage.view_center_y)) { view_to.center_y = (view.center_y = parseFloat(localStorage.view_center_y)); }
36 | if (!isNaN(localStorage.view_scale)) { view_to.scale = (view.scale = parseFloat(localStorage.view_scale)); }
37 | } catch (error) { }
38 |
39 | setInterval(function () {
40 | if (editor.editing) {
41 | // TODO: should probably only save if you pan/zoom
42 | localStorage.view_center_x = view.center_x;
43 | localStorage.view_center_y = view.center_y;
44 | localStorage.view_scale = view_to.scale;
45 | }
46 | }, 200);
47 |
48 | const animate = function () {
49 | if (window.CRASHED) { return; }
50 | requestAnimationFrame(animate);
51 |
52 | if (canvas.width !== innerWidth) { canvas.width = innerWidth; }
53 | if (canvas.height !== innerHeight) { canvas.height = innerHeight; }
54 |
55 | ctx.clearRect(0, 0, canvas.width, canvas.height);
56 |
57 | if (editor.editing && (editor.entities_bar.hovered_cell || ((editor.hovered_points.length || editor.hovered_entities.length) && !editor.selection_box))) {
58 | canvas.classList.add("grabbable");
59 | } else {
60 | canvas.classList.remove("grabbable");
61 | }
62 |
63 | if (!editor.editing) {
64 | for (var entity of world.entities) { // if (entity !== editor.editing_entity && !editor.dragging_entities.contains(entity)) {
65 | entity.step(world);
66 | }
67 |
68 | // TODO: allow margin of offcenteredness
69 | // player = world.getEntitiesOfType(Player)[0];
70 | // view_to.center_x = player.x;
71 | // view_to.center_y = player.y;
72 | }
73 |
74 | view.width = canvas.width;
75 | view.height = canvas.height;
76 |
77 | view.easeTowards(view_to, view_smoothness);
78 | if (editor.editing) { editor.step(); }
79 | mouse.resetForNextStep();
80 |
81 | world.drawBackground(ctx, view);
82 | ctx.save();
83 | ctx.translate(canvas.width / 2, canvas.height / 2);
84 | ctx.scale(view.scale, view.scale);
85 | ctx.translate(-view.center_x, -view.center_y);
86 |
87 | world.draw(ctx, view);
88 | if (editor.editing) { editor.draw(ctx, view); }
89 |
90 | ctx.restore();
91 |
92 | editor.updateGUI();
93 |
94 | // keyboard.resetForNextStep();
95 |
96 | };
97 | animate();
98 |
--------------------------------------------------------------------------------
/source/arcs-overlap.js:
--------------------------------------------------------------------------------
1 | const TAU = Math.PI * 2;
2 | const EPSILON = 0.0001;
3 |
4 | export function arcsOverlap(startAngle1, angleDiff1, startAngle2, angleDiff2) {
5 |
6 | // Handle zero-length arcs
7 | // This is before full circle handling in order to match
8 | // the test harness's notion of overlap which is based
9 | // on image comparison. If any of the arcs are zero-length,
10 | // no visual overlap is possible.
11 | // if (angleDiff1 === 0 || angleDiff2 === 0) {
12 | if (Math.abs(angleDiff1) < EPSILON || Math.abs(angleDiff2) < EPSILON) {
13 | // return startAngle1 === startAngle2;
14 | // return Math.abs(startAngle1 - startAngle2) < EPSILON;
15 | return false;
16 | }
17 |
18 | // Handle full circles
19 | // (This should be before floating point angle difference shrinking)
20 | if (Math.abs(angleDiff1) >= TAU || Math.abs(angleDiff2) >= TAU) {
21 | return true;
22 | }
23 |
24 | // For floating point imprecision, shrink arc lengths slightly
25 | angleDiff1 -= Math.sign(angleDiff1) * EPSILON;
26 | angleDiff2 -= Math.sign(angleDiff2) * EPSILON;
27 |
28 | // Simplify: only the difference in start angles matters,
29 | // not their absolute angles
30 | // This gives me confidence that I don't need to test
31 | // varying both parameters.
32 | startAngle2 -= startAngle1;
33 | startAngle1 = 0;
34 |
35 | // Normalize: wrap all angles to [0, 2π)
36 | startAngle1 = (startAngle1 % TAU + TAU) % TAU;
37 | startAngle2 = (startAngle2 % TAU + TAU) % TAU;
38 |
39 | // Normalize: make sure angleDiff1 is positive
40 | if (angleDiff1 < 0) {
41 | angleDiff1 *= -1;
42 | startAngle1 *= -1;
43 | angleDiff2 *= -1;
44 | startAngle2 *= -1;
45 | }
46 |
47 | {
48 | // Calculate the end angles of each arc
49 | const endAngle1 = startAngle1 + angleDiff1;
50 | const endAngle2 = startAngle2 + angleDiff2;
51 |
52 | for (let i = 0; i < 2; i++) {
53 | if (startAngle1 < startAngle2 + TAU * i && startAngle2 + TAU * i < endAngle1) {
54 | return true;
55 | }
56 | if (startAngle2 < startAngle1 + TAU * i && startAngle1 + TAU * i < endAngle2) {
57 | return true;
58 | }
59 | if (startAngle1 < endAngle2 + TAU * i && endAngle2 + TAU * i < endAngle1) {
60 | return true;
61 | }
62 | if (startAngle2 < endAngle1 + TAU * i && endAngle1 + TAU * i < endAngle2) {
63 | return true;
64 | }
65 | }
66 | }
67 |
68 | // Normalize: make sure angleDiff2 is positive
69 | if (angleDiff2 < 0) {
70 | angleDiff1 *= -1;
71 | startAngle1 *= -1;
72 | angleDiff2 *= -1;
73 | startAngle2 *= -1;
74 | }
75 |
76 | {
77 | // Calculate the end angles of each arc
78 | const endAngle1 = startAngle1 + angleDiff1;
79 | const endAngle2 = startAngle2 + angleDiff2;
80 |
81 | for (let i = 0; i < 2; i++) {
82 | if (startAngle1 < startAngle2 + TAU * i && startAngle2 + TAU * i < endAngle1) {
83 | return true;
84 | }
85 | if (startAngle2 < startAngle1 + TAU * i && startAngle1 + TAU * i < endAngle2) {
86 | return true;
87 | }
88 | if (startAngle1 < endAngle2 + TAU * i && endAngle2 + TAU * i < endAngle1) {
89 | return true;
90 | }
91 | if (startAngle2 < endAngle1 + TAU * i && endAngle1 + TAU * i < endAngle2) {
92 | return true;
93 | }
94 | }
95 | }
96 |
97 | return false;
98 | }
99 |
--------------------------------------------------------------------------------
/source/structure/BoneStructure.coffee:
--------------------------------------------------------------------------------
1 | import Structure from "./Structure.coffee"
2 |
3 | export default class BoneStructure extends Structure
4 |
5 | addPoint: (name)->
6 | if @points[name]
7 | throw new Error "point/segment '#{name}' already exists adding point '#{name}'"
8 | @points[name] = {x: 0, y: 0, name}
9 |
10 | addSegment: (def)->
11 | {from, to, name} = def
12 | to ?= name
13 | if @segments[name]
14 | throw new Error "segment '#{name}' already exists adding segment '#{name}'"
15 | if @points[to]
16 | throw new Error "point/segment '#{name}' already exists adding segment '#{name}'"
17 | unless @points[from]
18 | throw new Error "point/segment '#{from}' does not exist yet adding segment '#{name}'"
19 | @points[to] = {x: 0, y: 0, name: to}
20 | @segments[name] = {a: @points[from], b: @points[to], from, to, name}
21 | @segments[name][k] = v for k, v of def when v?
22 | return name
23 |
24 | stepLayout: ({center, repel, gravity, collision, velocity}={})->
25 | forces = {}
26 |
27 | center_around = {x: 0, y: 0}
28 |
29 | for point_name, point of @points
30 | forces[point_name] = {x: 0, y: 0}
31 |
32 | if center
33 | dx = center_around.x - point.x
34 | dy = center_around.y - point.y
35 | dist = Math.sqrt(dx * dx + dy * dy)
36 | forces[point_name].x += dx * dist / 100000
37 | forces[point_name].y += dy * dist / 100000
38 |
39 | if repel
40 | for other_point_name, other_point of @points
41 | dx = other_point.x - point.x
42 | dy = other_point.y - point.y
43 | dist = Math.sqrt(dx * dx + dy * dy)
44 | delta_dist = 5 - dist
45 | unless delta_dist is 0
46 | forces[point_name].x += dx / delta_dist / 1000
47 | forces[point_name].y += dy / delta_dist / 1000
48 |
49 | if gravity
50 | forces[point_name].y += gravity
51 |
52 | for segment_name, segment of @segments
53 | dx = segment.a.x - segment.b.x
54 | dy = segment.a.y - segment.b.y
55 | dist = Math.sqrt(dx * dx + dy * dy)
56 | delta_dist = dist - (segment.length ? 50)
57 | delta_dist = Math.min(delta_dist, 100)
58 | forces[segment.a.name].x -= dx * delta_dist / 1000
59 | forces[segment.a.name].y -= dy * delta_dist / 1000
60 | forces[segment.b.name].x += dx * delta_dist / 1000
61 | forces[segment.b.name].y += dy * delta_dist / 1000
62 |
63 | for point_name, force of forces
64 | point = @points[point_name]
65 | if collision
66 | point.vx ?= 0
67 | point.vy ?= 0
68 | point.vx += force.x
69 | point.vy += force.y
70 | move_x = point.vx
71 | move_y = point.vy
72 | resolution = 0.5
73 | while Math.abs(move_x) > resolution
74 | go = Math.sign(move_x) * resolution
75 | if collision({x: point.x + go, y: point.y})
76 | point.vx *= 0.99
77 | if collision({x: point.x + go, y: point.y - 1})
78 | break
79 | else
80 | point.y -= 1
81 | move_x -= go
82 | point.x += go
83 | while Math.abs(move_y) > resolution
84 | go = Math.sign(move_y) * resolution
85 | if collision({x: point.x, y: point.y + go})
86 | point.vy *= 0.9 # as opposed to `point.vy = 0` so it sticks to the ground when going downhill
87 | break
88 | move_y -= go
89 | point.y += go
90 | else
91 | point.x += force.x
92 | point.y += force.y
93 |
--------------------------------------------------------------------------------
/source/icons/push-hand-with-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
63 |
--------------------------------------------------------------------------------
/source/components/Anim.coffee:
--------------------------------------------------------------------------------
1 |
2 | # awkward component Anim represents a pose OR an animation OR an animation frame (which is an unnamed pose)
3 |
4 | import {Component} from "react"
5 | import ReactDOM from "react-dom"
6 | import E from "react-script"
7 | import EntityPreview from "./EntityPreview.coffee"
8 | import Entity from "../base-entities/Entity.coffee"
9 | import renameObjectKey from "../rename-object-key.coffee"
10 | import deleteIcon from "../icons/delete.svg"
11 |
12 | export default class Anim extends Component
13 | constructor: ->
14 | super()
15 |
16 | render: ->
17 | {entity, EntityClass, name, type_of_anims, selected, select, delete_item, update, editor} = @props
18 | max_width = 200
19 | max_height = 100
20 | E "article",
21 | class: {selected}
22 | onClick: (e)=>
23 | return if e.defaultPrevented
24 | select()
25 | update()
26 | if name is "Current Pose"
27 | E "h1.name", name
28 | else
29 | # TODO: for animation-frames, instead of a textfield have a reorder handle and a duration control
30 | # well, a reorder handle might be nice for the other anims too
31 | E ".title-bar",
32 | style: {maxWidth: max_width}
33 | E ".mdl-textfield.mdl-js-textfield.name",
34 | ref: (@mdl_textfield_el)=>
35 | E "input.mdl-textfield__input",
36 | value: name.replace(/TEMP_NAME_SENTINEL\d*$/, "")
37 | ref: (@name_input_el)=>
38 | dataInvalidNotUnique: name.includes("TEMP_NAME_SENTINEL")
39 | required: true
40 | onFocus: (e)=>
41 | @name_input_el.reportValidity()
42 | onInput: (e)=>
43 | new_name = e.target.value
44 | # TODO: use error classes and messages instead of intrusive alerts
45 | if type_of_anims is "animations"
46 | if EntityClass.animations[new_name]
47 | # editor.warn("There's already an animation with the name '#{new_name}'")
48 | # setCustomValidity is better, more contextual.
49 | @name_input_el.setCustomValidity("There's already an animation with the name '#{new_name}'")
50 | needs_temp_name = true
51 | else if type_of_anims is "poses"
52 | if EntityClass.poses[new_name]
53 | # editor.warn("There's already a pose with the name '#{new_name}'")
54 | @name_input_el.setCustomValidity("There's already a pose with the name '#{new_name}'")
55 | needs_temp_name = true
56 | else
57 | alert("This shouldn't happen. Unknown type: #{type_of_anims}")
58 | return
59 |
60 | if needs_temp_name
61 | new_name += "TEMP_NAME_SENTINEL"
62 | while EntityClass[type_of_anims][new_name]
63 | new_name += "1"
64 | else
65 | @name_input_el.setCustomValidity("")
66 | @name_input_el.reportValidity()
67 |
68 | anims_object = EntityClass[type_of_anims]
69 | renameObjectKey(anims_object, name, new_name)
70 | editor.editing_entity_anim_name = new_name
71 | Entity.saveAnimations(EntityClass)
72 |
73 | # cause rerender immediately so cursor doesn't get moved to the end of the field
74 | update()
75 | E "label.mdl-textfield__label", "Name..."
76 | E "button.mdl-button.mdl-js-button.mdl-button--icon.mdl-color-text--grey-600.delete",
77 | onClick: (e)=>
78 | e.preventDefault()
79 | delete_item()
80 | Entity.saveAnimations(EntityClass)
81 | # E "i.material-icons", "delete"
82 | E "img", src: deleteIcon
83 | E EntityPreview, {
84 | entity, max_width, max_height
85 | ref: (@entity_preview)=>
86 | }
87 |
88 | componentDidMount: ->
89 | componentHandler.upgradeElement(ReactDOM.findDOMNode(@mdl_textfield_el)) if @mdl_textfield_el?
90 |
91 | if @name_input_el?.dataset.invalidNotUnique
92 | @name_input_el.setCustomValidity("Please enter a unique name.")
93 |
--------------------------------------------------------------------------------
/TESTS.txt:
--------------------------------------------------------------------------------
1 | # "Tests"
2 |
3 | these could be considered manual tests
4 | and some could be automated perhaps
5 | but it's also a sketch of functionality
6 | not all of these things are implemented,
7 | so if one of these doesn't work, it's not necessarily a *bug*
8 |
9 |
10 | ## Editor Behavior
11 |
12 | drag with the middle mouse button to pan the view
13 | (with momentum, wee!)
14 | zoom in and out with the mouse wheel
15 | (with the mouse anchored in the world)
16 |
17 | while editing an entity
18 | drag outside of the entity to select points (w/ a selection box)
19 | double click outside of the entity to stop editing the entity
20 | (another entity you click on should not be selected)
21 | (the entity should be deselected)
22 | double clicking on the entity should not stop editing the entity
23 | drag on a selected point to move all selected points
24 | drag on a non-selected point to select that point and move it
25 | alt+drag to drag the selection from anywhere
26 | click on a point to select that point
27 | (even when it's one of multiple points in the selection)
28 | shift+click or ctrl+click on a point to toggle the selected state of that point
29 | shift+drag from anywhere to select points (w/ a selection box)
30 | press delete to delete selected points
31 | otherwise
32 | press delete to delete selected entities
33 | with selected entities
34 | drag on a selected entity to move all selected entities
35 | alt+drag to drag the selection from anywhere
36 | double click on a selected entity to edit the entity
37 | (should always make it the only selected entity)
38 | click on a selected entity to make it the only selected entity
39 | drag on a non-selected entity to select that entity and move it
40 | drag outside of any entity to select entities (w/ a selection box)
41 | click on an entity to select that entity
42 | shift+click or ctrl+click on an entity to toggle the selected state of that entity
43 | shift+drag from anywhere to select entities (w/ a selection box)
44 |
45 | drag from the entities bar to create and place an entity
46 | click on an entity in the bar to create it and have it placed randomly offscreen in the middle of nowhere
47 | (or not)
48 | (the cursor should be enough indication that you need to drag)
49 |
50 | only what will be dragged should ever be shown as hovered
51 | when there are multiple points within the minimum range for dragging, the closest should be hovered
52 | when there are multiple entities within the minimum range for dragging, the one on top should probably be hovered
53 | you can drag a selection to access entities that are behind large entities such as terrain or a large tree
54 |
55 | while dragging an entity, the entities bar should be hidden
56 |
57 | when starting editing an entity, you should not also start dragging a point
58 |
59 | delete, undo, redo, etc. should work while dragging entities or points
60 | minimum drag distances should be based on view positions, not world positions
61 | undo states should only be created once a drag starts
62 |
63 | pressing esc should either
64 | 1. cancel a selection/dragging gesture, resetting to the state before the gesture (including removing an entity dragged from the entities bar),
65 | 2. deselect any points,
66 | 3. exit entity editing mode, or
67 | 4. deselect any entities,
68 | in that order of priority
69 | alt+tabbing away from the window or exiting edit mode should cancel any gesture in the same way as esc, and should reset the hover state
70 |
71 | entities and points should have hover styles
72 |
73 | double clicks where the first click was not on the same entity as the second should be rejected
74 |
75 | MMB-dragging from the entities bar should either work or not
76 | it should not start a drag but not drag until you mouse off of the entities bar
77 |
78 | there should be a way to regenerate an entity
79 | it could be something like right click
80 | it should work for both placed entities and entities in the entities bar
81 | entity previews in the entities bar could show the exact random entity you would receive (and then generate a new one in the bar)
82 |
83 | context menus
84 |
--------------------------------------------------------------------------------
/source/icons/sculpt.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
63 |
--------------------------------------------------------------------------------
/source/jsMenus/jsMenus.css:
--------------------------------------------------------------------------------
1 |
2 | html, body {
3 | margin: 0;
4 | height: 100%;
5 | }
6 | body { display: flex; flex-direction: column }
7 | .menubar { flex: 0 0 22px }
8 | div.below-menubar { flex: 1 1 0; min-height: 0;}
9 |
10 | .nwjs-menu {
11 | font-family: 'Helvetica Neue', HelveticaNeue, 'TeX Gyre Heros', TeXGyreHeros, FreeSans, 'Nimbus Sans L', 'Liberation Sans', Arimo, Helvetica, Arial, sans-serif;
12 | font-size: 14px;
13 | color: #2c2c2c;
14 | -webkit-user-select: none;
15 | user-select: none;
16 | -webkit-font-smoothing: subpixel-antialiased;
17 | font-weight: 400;
18 | }
19 |
20 | .contextmenu {
21 | min-width: 100px;
22 | background-color: #fafafa;
23 | position: fixed;
24 | opacity: 0;
25 | transition: opacity 250ms;
26 | margin: 0;
27 | padding: 0 0;
28 | list-style: none;
29 | pointer-events: none;
30 | border: 1px rgba(191, 191, 191, 0.8) solid;
31 | box-shadow: rgba(43, 43, 43, 0.34) 1px 1px 11px 0px;
32 | z-index: 2147483647;
33 | }
34 |
35 | .contextmenu {
36 | opacity: 1;
37 | transition: opacity 30ms;
38 | pointer-events: all;
39 | }
40 |
41 | .contextmenu.submenu {
42 | transition: opacity 250ms;
43 | }
44 |
45 | .contextmenu.submenu {
46 | transition: opacity 150ms;
47 | transition-timing-function: step-end;
48 | }
49 |
50 | .menu-item.normal,
51 | .menu-item.checkbox,
52 | .menu-item.radio {
53 | cursor: default;
54 | padding: 2px 0;
55 | box-sizing: border-box;
56 | position: relative;
57 | display: flex;
58 | flex-direction: row;
59 | flex-wrap: nowrap;
60 | justify-content: flex-start;
61 | align-content: stretch;
62 | align-items: flex-start;
63 | width: 100%;
64 | }
65 |
66 | .contextmenu .menu-item.active,
67 | .menu-item.normal.submenu-active {
68 | background-color: #499BFE;
69 | color: #fff;
70 | }
71 |
72 | .menu-item.normal > div,
73 | .menu-item.checkbox > div,
74 | .menu-item.radio > div {
75 | align-self: center;
76 | vertical-align: middle;
77 | display: inline-flex;
78 | justify-content: flex-start;
79 | flex-shrink: 0;
80 | }
81 |
82 | .menu-item.normal .icon {
83 | display: inline-flex;
84 | vertical-align: middle;
85 | max-width: 16px;
86 | max-height: 16px;
87 | align-self: center;
88 | }
89 |
90 | li.menu-item.separator {
91 | height: 2px;
92 | background-color: rgba(128, 128, 128, 0.2);
93 | margin: 5px 0;
94 | }
95 |
96 | .menu-item .modifiers,
97 | .menu-item .icon-wrap,
98 | .menu-item .checkmark {
99 | display: inline-flex;
100 | align-items: center;
101 | vertical-align: middle;
102 | }
103 |
104 | .menu-item .checkmark {
105 | width: 22px;
106 | }
107 |
108 | .menu-item .modifiers {
109 | box-sizing: border-box;
110 | padding: 0 6px;
111 | text-align: right;
112 | order: 0;
113 | flex: 0 0 auto;
114 | align-self: center;
115 | }
116 |
117 | .menu-item .label {
118 | padding: 0 22px 0 0;
119 | order: 0;
120 | flex: 1 0 auto;
121 | align-self: center;
122 | }
123 |
124 | .menu-item.disabled,
125 | .menu-item.disabled:hover,
126 | .contextmenu .menu-item.disabled:hover {
127 | color: #ababab;
128 | }
129 |
130 | .menu-item.disabled:hover,
131 | .contextmenu .menu-item.disabled:hover {
132 | background-color: transparent;
133 | }
134 |
135 | .menu-item .icon-wrap {
136 | padding: 0 6px 0 0;
137 | display: inline-flex;
138 | align-self: center;
139 | }
140 |
141 | .menu-item .label-text {
142 | align-items: center;
143 | vertical-align: middle;
144 | }
145 |
146 | .menu-item.checkbox.checked .checkmark::before {
147 | content: '✔';
148 | text-align: center;
149 | width: 100%;
150 | }
151 |
152 | .menu-item.radio.checked .checkmark::before {
153 | content: '⊚';
154 | text-align: center;
155 | width: 100%;
156 | }
157 |
158 | .menubar {
159 | height: 22px;
160 | margin: 0;
161 | padding: 0;
162 | top: 0;
163 | left: 0;
164 | right: 0;
165 | background-color: #eee;
166 | z-index: 2147483647;
167 | }
168 |
169 | .menubar .menu-item.normal {
170 | display: inline-block;
171 | width: auto;
172 | height: 100%;
173 | }
174 |
175 | .menubar .menu-item.normal > div {
176 | vertical-align: top;
177 | }
178 |
179 | .menubar .menu-item.normal .checkmark,
180 | .menubar .menu-item.normal .modifiers {
181 | display: none;
182 | }
183 |
184 | .menubar .menu-item.normal .label {
185 | padding: 0 9px;
186 | }
187 |
188 | .contextmenu.menubar-submenu {
189 | transition: opacity 0ms;
190 | }
191 |
192 | /* Mac only?
193 | .contextmenu {
194 | border-radius: 7px;
195 | }
196 | .contextmenu.menubar-submenu {
197 | border-radius: 0 0 7px 7px;
198 | }
199 | */
200 |
--------------------------------------------------------------------------------
/examples/esm-pixi/source/main.js:
--------------------------------------------------------------------------------
1 | import { View, Mouse, Editor } from "skele2d";
2 | import { Application as PIXI_Application } from "pixi.js";
3 | import World from "./World.js";
4 | import Rock from "./entities/terrain/Rock.js";
5 | import Snow from "./entities/terrain/Snow.js";
6 | import Snake from "./entities/Snake.js";
7 | // import keyboard from "./keyboard.js";
8 |
9 | Math.seedrandom("A world");
10 |
11 | const world = new World;
12 |
13 | const terrain = new Snow;
14 | world.entities.push(terrain);
15 | terrain.x = 0;
16 | terrain.y = 0;
17 | terrain.generate();
18 |
19 | const snake = new Snake;
20 | world.entities.push(snake);
21 |
22 | const app = new PIXI_Application({ resizeTo: window });
23 | document.body.appendChild(app.view);
24 | app.view.style.position = "absolute";
25 | app.view.style.top = "0";
26 | app.view.style.left = "0";
27 | // for PixiJS Devtools
28 | globalThis.__PIXI_APP__ = app;
29 |
30 | const canvas = document.createElement("canvas");
31 | canvas.style.position = "absolute";
32 | canvas.style.top = "0";
33 | canvas.style.left = "0";
34 | document.body.appendChild(canvas);
35 | const ctx = canvas.getContext("2d");
36 |
37 | const view = new View;
38 | const view_to = new View;
39 | const view_smoothness = 7;
40 | const mouse = new Mouse(canvas);
41 |
42 | const editor = new Editor(world, view, view_to, canvas, mouse);
43 | try {
44 | editor.load();
45 | } catch (e) {
46 | console.error?.("Failed to load save:", e);
47 | }
48 |
49 | try {
50 | if (!isNaN(localStorage.view_center_x)) { view_to.center_x = (view.center_x = parseFloat(localStorage.view_center_x)); }
51 | if (!isNaN(localStorage.view_center_y)) { view_to.center_y = (view.center_y = parseFloat(localStorage.view_center_y)); }
52 | if (!isNaN(localStorage.view_scale)) { view_to.scale = (view.scale = parseFloat(localStorage.view_scale)); }
53 | } catch (error) { }
54 |
55 | setInterval(function () {
56 | if (editor.editing) {
57 | // TODO: should probably only save if you pan/zoom
58 | localStorage.view_center_x = view.center_x;
59 | localStorage.view_center_y = view.center_y;
60 | localStorage.view_scale = view_to.scale;
61 | }
62 | }, 200);
63 |
64 | let old_entities_list = [];
65 |
66 | app.ticker.add(function () {
67 | if (window.CRASHED) { return; }
68 |
69 | if (canvas.width !== innerWidth) { canvas.width = innerWidth; }
70 | if (canvas.height !== innerHeight) { canvas.height = innerHeight; }
71 |
72 | ctx.clearRect(0, 0, canvas.width, canvas.height);
73 |
74 | if (editor.editing && (editor.entities_bar.hovered_cell || ((editor.hovered_points.length || editor.hovered_entities.length) && !editor.selection_box))) {
75 | canvas.classList.add("grabbable");
76 | } else {
77 | canvas.classList.remove("grabbable");
78 | }
79 |
80 | if (!editor.editing) {
81 | for (var entity of world.entities) { // if (entity !== editor.editing_entity && !editor.dragging_entities.contains(entity)) {
82 | entity.step(world);
83 | }
84 |
85 | // TODO: allow margin of offcenteredness
86 | // player = world.getEntitiesOfType(Player)[0];
87 | // view_to.center_x = player.x;
88 | // view_to.center_y = player.y;
89 | }
90 |
91 | view.width = canvas.width;
92 | view.height = canvas.height;
93 |
94 | view.easeTowards(view_to, view_smoothness);
95 | if (editor.editing) { editor.step(); }
96 | mouse.resetForNextStep();
97 |
98 | world.drawBackground(ctx, view);
99 | ctx.save();
100 | ctx.translate(canvas.width / 2, canvas.height / 2);
101 | ctx.scale(view.scale, view.scale);
102 | ctx.translate(-view.center_x, -view.center_y);
103 |
104 | // align PIXI scene to the CanvasRenderingContext2D scene
105 | app.stage.position.x = -view.center_x * view.scale + canvas.width / 2;
106 | app.stage.position.y = -view.center_y * view.scale + canvas.height / 2;
107 | app.stage.scale.x = view.scale;
108 | app.stage.scale.y = view.scale;
109 |
110 | // Note: this doesn't cause PIXI to actually redraw.
111 | world.pixiUpdate(app.stage, app.ticker);
112 |
113 | world.draw(ctx, view);
114 | if (editor.editing) { editor.draw(ctx, view); }
115 |
116 | ctx.restore();
117 |
118 | editor.updateGUI();
119 |
120 | // Destroy entities that were removed from the world.
121 | // This handles undo/redo and delete, although I also have a destroyed setter in Snake.coffee which handles delete.
122 | // TODO: make Skele2D call a destroy method on entities when they're removed from the world.
123 | for (entity of Array.from(old_entities_list)) {
124 | if (!Array.from(world.entities).includes(entity)) {
125 | if (typeof entity.destroy === 'function') {
126 | entity.destroy();
127 | }
128 | }
129 | }
130 | old_entities_list = [...world.entities];
131 |
132 | // keyboard.resetForNextStep();
133 |
134 | });
135 |
--------------------------------------------------------------------------------
/examples/esm/lib/simplex-noise.min.js:
--------------------------------------------------------------------------------
1 | /*! simplex-noise.js: copyright 2012 Jonas Wagner, licensed under a MIT license. See https://github.com/jwagner/simplex-noise.js for details */
2 | !function(){"use strict";function SimplexNoise(random){random||(random=Math.random),this.p=new Uint8Array(256),this.perm=new Uint8Array(512),this.permMod12=new Uint8Array(512);for(var i=0;256>i;i++)this.p[i]=256*random();for(i=0;512>i;i++)this.perm[i]=this.p[255&i],this.permMod12[i]=this.perm[i]%12}var F2=.5*(Math.sqrt(3)-1),G2=(3-Math.sqrt(3))/6,F3=1/3,G3=1/6,F4=(Math.sqrt(5)-1)/4,G4=(5-Math.sqrt(5))/20;SimplexNoise.prototype={grad3:new Float32Array([1,1,0,-1,1,0,1,-1,0,-1,-1,0,1,0,1,-1,0,1,1,0,-1,-1,0,-1,0,1,1,0,-1,1,0,1,-1,0,-1,-1]),grad4:new Float32Array([0,1,1,1,0,1,1,-1,0,1,-1,1,0,1,-1,-1,0,-1,1,1,0,-1,1,-1,0,-1,-1,1,0,-1,-1,-1,1,0,1,1,1,0,1,-1,1,0,-1,1,1,0,-1,-1,-1,0,1,1,-1,0,1,-1,-1,0,-1,1,-1,0,-1,-1,1,1,0,1,1,1,0,-1,1,-1,0,1,1,-1,0,-1,-1,1,0,1,-1,1,0,-1,-1,-1,0,1,-1,-1,0,-1,1,1,1,0,1,1,-1,0,1,-1,1,0,1,-1,-1,0,-1,1,1,0,-1,1,-1,0,-1,-1,1,0,-1,-1,-1,0]),noise2D:function(xin,yin){var i1,j1,permMod12=this.permMod12,perm=this.perm,grad3=this.grad3,n0=0,n1=0,n2=0,s=(xin+yin)*F2,i=Math.floor(xin+s),j=Math.floor(yin+s),t=(i+j)*G2,X0=i-t,Y0=j-t,x0=xin-X0,y0=yin-Y0;x0>y0?(i1=1,j1=0):(i1=0,j1=1);var x1=x0-i1+G2,y1=y0-j1+G2,x2=x0-1+2*G2,y2=y0-1+2*G2,ii=255&i,jj=255&j,t0=.5-x0*x0-y0*y0;if(t0>=0){var gi0=3*permMod12[ii+perm[jj]];t0*=t0,n0=t0*t0*(grad3[gi0]*x0+grad3[gi0+1]*y0)}var t1=.5-x1*x1-y1*y1;if(t1>=0){var gi1=3*permMod12[ii+i1+perm[jj+j1]];t1*=t1,n1=t1*t1*(grad3[gi1]*x1+grad3[gi1+1]*y1)}var t2=.5-x2*x2-y2*y2;if(t2>=0){var gi2=3*permMod12[ii+1+perm[jj+1]];t2*=t2,n2=t2*t2*(grad3[gi2]*x2+grad3[gi2+1]*y2)}return 70*(n0+n1+n2)},noise3D:function(xin,yin,zin){var n0,n1,n2,n3,i1,j1,k1,i2,j2,k2,permMod12=this.permMod12,perm=this.perm,grad3=this.grad3,s=(xin+yin+zin)*F3,i=Math.floor(xin+s),j=Math.floor(yin+s),k=Math.floor(zin+s),t=(i+j+k)*G3,X0=i-t,Y0=j-t,Z0=k-t,x0=xin-X0,y0=yin-Y0,z0=zin-Z0;x0>=y0?y0>=z0?(i1=1,j1=0,k1=0,i2=1,j2=1,k2=0):x0>=z0?(i1=1,j1=0,k1=0,i2=1,j2=0,k2=1):(i1=0,j1=0,k1=1,i2=1,j2=0,k2=1):z0>y0?(i1=0,j1=0,k1=1,i2=0,j2=1,k2=1):z0>x0?(i1=0,j1=1,k1=0,i2=0,j2=1,k2=1):(i1=0,j1=1,k1=0,i2=1,j2=1,k2=0);var x1=x0-i1+G3,y1=y0-j1+G3,z1=z0-k1+G3,x2=x0-i2+2*G3,y2=y0-j2+2*G3,z2=z0-k2+2*G3,x3=x0-1+3*G3,y3=y0-1+3*G3,z3=z0-1+3*G3,ii=255&i,jj=255&j,kk=255&k,t0=.6-x0*x0-y0*y0-z0*z0;if(0>t0)n0=0;else{var gi0=3*permMod12[ii+perm[jj+perm[kk]]];t0*=t0,n0=t0*t0*(grad3[gi0]*x0+grad3[gi0+1]*y0+grad3[gi0+2]*z0)}var t1=.6-x1*x1-y1*y1-z1*z1;if(0>t1)n1=0;else{var gi1=3*permMod12[ii+i1+perm[jj+j1+perm[kk+k1]]];t1*=t1,n1=t1*t1*(grad3[gi1]*x1+grad3[gi1+1]*y1+grad3[gi1+2]*z1)}var t2=.6-x2*x2-y2*y2-z2*z2;if(0>t2)n2=0;else{var gi2=3*permMod12[ii+i2+perm[jj+j2+perm[kk+k2]]];t2*=t2,n2=t2*t2*(grad3[gi2]*x2+grad3[gi2+1]*y2+grad3[gi2+2]*z2)}var t3=.6-x3*x3-y3*y3-z3*z3;if(0>t3)n3=0;else{var gi3=3*permMod12[ii+1+perm[jj+1+perm[kk+1]]];t3*=t3,n3=t3*t3*(grad3[gi3]*x3+grad3[gi3+1]*y3+grad3[gi3+2]*z3)}return 32*(n0+n1+n2+n3)},noise4D:function(x,y,z,w){var n0,n1,n2,n3,n4,perm=(this.permMod12,this.perm),grad4=this.grad4,s=(x+y+z+w)*F4,i=Math.floor(x+s),j=Math.floor(y+s),k=Math.floor(z+s),l=Math.floor(w+s),t=(i+j+k+l)*G4,X0=i-t,Y0=j-t,Z0=k-t,W0=l-t,x0=x-X0,y0=y-Y0,z0=z-Z0,w0=w-W0,rankx=0,ranky=0,rankz=0,rankw=0;x0>y0?rankx++:ranky++,x0>z0?rankx++:rankz++,x0>w0?rankx++:rankw++,y0>z0?ranky++:rankz++,y0>w0?ranky++:rankw++,z0>w0?rankz++:rankw++;var i1,j1,k1,l1,i2,j2,k2,l2,i3,j3,k3,l3;i1=rankx>=3?1:0,j1=ranky>=3?1:0,k1=rankz>=3?1:0,l1=rankw>=3?1:0,i2=rankx>=2?1:0,j2=ranky>=2?1:0,k2=rankz>=2?1:0,l2=rankw>=2?1:0,i3=rankx>=1?1:0,j3=ranky>=1?1:0,k3=rankz>=1?1:0,l3=rankw>=1?1:0;var x1=x0-i1+G4,y1=y0-j1+G4,z1=z0-k1+G4,w1=w0-l1+G4,x2=x0-i2+2*G4,y2=y0-j2+2*G4,z2=z0-k2+2*G4,w2=w0-l2+2*G4,x3=x0-i3+3*G4,y3=y0-j3+3*G4,z3=z0-k3+3*G4,w3=w0-l3+3*G4,x4=x0-1+4*G4,y4=y0-1+4*G4,z4=z0-1+4*G4,w4=w0-1+4*G4,ii=255&i,jj=255&j,kk=255&k,ll=255&l,t0=.6-x0*x0-y0*y0-z0*z0-w0*w0;if(0>t0)n0=0;else{var gi0=perm[ii+perm[jj+perm[kk+perm[ll]]]]%32*4;t0*=t0,n0=t0*t0*(grad4[gi0]*x0+grad4[gi0+1]*y0+grad4[gi0+2]*z0+grad4[gi0+3]*w0)}var t1=.6-x1*x1-y1*y1-z1*z1-w1*w1;if(0>t1)n1=0;else{var gi1=perm[ii+i1+perm[jj+j1+perm[kk+k1+perm[ll+l1]]]]%32*4;t1*=t1,n1=t1*t1*(grad4[gi1]*x1+grad4[gi1+1]*y1+grad4[gi1+2]*z1+grad4[gi1+3]*w1)}var t2=.6-x2*x2-y2*y2-z2*z2-w2*w2;if(0>t2)n2=0;else{var gi2=perm[ii+i2+perm[jj+j2+perm[kk+k2+perm[ll+l2]]]]%32*4;t2*=t2,n2=t2*t2*(grad4[gi2]*x2+grad4[gi2+1]*y2+grad4[gi2+2]*z2+grad4[gi2+3]*w2)}var t3=.6-x3*x3-y3*y3-z3*z3-w3*w3;if(0>t3)n3=0;else{var gi3=perm[ii+i3+perm[jj+j3+perm[kk+k3+perm[ll+l3]]]]%32*4;t3*=t3,n3=t3*t3*(grad4[gi3]*x3+grad4[gi3+1]*y3+grad4[gi3+2]*z3+grad4[gi3+3]*w3)}var t4=.6-x4*x4-y4*y4-z4*z4-w4*w4;if(0>t4)n4=0;else{var gi4=perm[ii+1+perm[jj+1+perm[kk+1+perm[ll+1]]]]%32*4;t4*=t4,n4=t4*t4*(grad4[gi4]*x4+grad4[gi4+1]*y4+grad4[gi4+2]*z4+grad4[gi4+3]*w4)}return 27*(n0+n1+n2+n3+n4)}},"undefined"!=typeof define&&define.amd&&define(function(){return SimplexNoise}),"undefined"!=typeof exports?exports.SimplexNoise=SimplexNoise:"undefined"!=typeof window&&(window.SimplexNoise=SimplexNoise),"undefined"!=typeof module&&(module.exports=SimplexNoise)}();
--------------------------------------------------------------------------------
/examples/esm-pixi/lib/simplex-noise.min.js:
--------------------------------------------------------------------------------
1 | /*! simplex-noise.js: copyright 2012 Jonas Wagner, licensed under a MIT license. See https://github.com/jwagner/simplex-noise.js for details */
2 | !function(){"use strict";function SimplexNoise(random){random||(random=Math.random),this.p=new Uint8Array(256),this.perm=new Uint8Array(512),this.permMod12=new Uint8Array(512);for(var i=0;256>i;i++)this.p[i]=256*random();for(i=0;512>i;i++)this.perm[i]=this.p[255&i],this.permMod12[i]=this.perm[i]%12}var F2=.5*(Math.sqrt(3)-1),G2=(3-Math.sqrt(3))/6,F3=1/3,G3=1/6,F4=(Math.sqrt(5)-1)/4,G4=(5-Math.sqrt(5))/20;SimplexNoise.prototype={grad3:new Float32Array([1,1,0,-1,1,0,1,-1,0,-1,-1,0,1,0,1,-1,0,1,1,0,-1,-1,0,-1,0,1,1,0,-1,1,0,1,-1,0,-1,-1]),grad4:new Float32Array([0,1,1,1,0,1,1,-1,0,1,-1,1,0,1,-1,-1,0,-1,1,1,0,-1,1,-1,0,-1,-1,1,0,-1,-1,-1,1,0,1,1,1,0,1,-1,1,0,-1,1,1,0,-1,-1,-1,0,1,1,-1,0,1,-1,-1,0,-1,1,-1,0,-1,-1,1,1,0,1,1,1,0,-1,1,-1,0,1,1,-1,0,-1,-1,1,0,1,-1,1,0,-1,-1,-1,0,1,-1,-1,0,-1,1,1,1,0,1,1,-1,0,1,-1,1,0,1,-1,-1,0,-1,1,1,0,-1,1,-1,0,-1,-1,1,0,-1,-1,-1,0]),noise2D:function(xin,yin){var i1,j1,permMod12=this.permMod12,perm=this.perm,grad3=this.grad3,n0=0,n1=0,n2=0,s=(xin+yin)*F2,i=Math.floor(xin+s),j=Math.floor(yin+s),t=(i+j)*G2,X0=i-t,Y0=j-t,x0=xin-X0,y0=yin-Y0;x0>y0?(i1=1,j1=0):(i1=0,j1=1);var x1=x0-i1+G2,y1=y0-j1+G2,x2=x0-1+2*G2,y2=y0-1+2*G2,ii=255&i,jj=255&j,t0=.5-x0*x0-y0*y0;if(t0>=0){var gi0=3*permMod12[ii+perm[jj]];t0*=t0,n0=t0*t0*(grad3[gi0]*x0+grad3[gi0+1]*y0)}var t1=.5-x1*x1-y1*y1;if(t1>=0){var gi1=3*permMod12[ii+i1+perm[jj+j1]];t1*=t1,n1=t1*t1*(grad3[gi1]*x1+grad3[gi1+1]*y1)}var t2=.5-x2*x2-y2*y2;if(t2>=0){var gi2=3*permMod12[ii+1+perm[jj+1]];t2*=t2,n2=t2*t2*(grad3[gi2]*x2+grad3[gi2+1]*y2)}return 70*(n0+n1+n2)},noise3D:function(xin,yin,zin){var n0,n1,n2,n3,i1,j1,k1,i2,j2,k2,permMod12=this.permMod12,perm=this.perm,grad3=this.grad3,s=(xin+yin+zin)*F3,i=Math.floor(xin+s),j=Math.floor(yin+s),k=Math.floor(zin+s),t=(i+j+k)*G3,X0=i-t,Y0=j-t,Z0=k-t,x0=xin-X0,y0=yin-Y0,z0=zin-Z0;x0>=y0?y0>=z0?(i1=1,j1=0,k1=0,i2=1,j2=1,k2=0):x0>=z0?(i1=1,j1=0,k1=0,i2=1,j2=0,k2=1):(i1=0,j1=0,k1=1,i2=1,j2=0,k2=1):z0>y0?(i1=0,j1=0,k1=1,i2=0,j2=1,k2=1):z0>x0?(i1=0,j1=1,k1=0,i2=0,j2=1,k2=1):(i1=0,j1=1,k1=0,i2=1,j2=1,k2=0);var x1=x0-i1+G3,y1=y0-j1+G3,z1=z0-k1+G3,x2=x0-i2+2*G3,y2=y0-j2+2*G3,z2=z0-k2+2*G3,x3=x0-1+3*G3,y3=y0-1+3*G3,z3=z0-1+3*G3,ii=255&i,jj=255&j,kk=255&k,t0=.6-x0*x0-y0*y0-z0*z0;if(0>t0)n0=0;else{var gi0=3*permMod12[ii+perm[jj+perm[kk]]];t0*=t0,n0=t0*t0*(grad3[gi0]*x0+grad3[gi0+1]*y0+grad3[gi0+2]*z0)}var t1=.6-x1*x1-y1*y1-z1*z1;if(0>t1)n1=0;else{var gi1=3*permMod12[ii+i1+perm[jj+j1+perm[kk+k1]]];t1*=t1,n1=t1*t1*(grad3[gi1]*x1+grad3[gi1+1]*y1+grad3[gi1+2]*z1)}var t2=.6-x2*x2-y2*y2-z2*z2;if(0>t2)n2=0;else{var gi2=3*permMod12[ii+i2+perm[jj+j2+perm[kk+k2]]];t2*=t2,n2=t2*t2*(grad3[gi2]*x2+grad3[gi2+1]*y2+grad3[gi2+2]*z2)}var t3=.6-x3*x3-y3*y3-z3*z3;if(0>t3)n3=0;else{var gi3=3*permMod12[ii+1+perm[jj+1+perm[kk+1]]];t3*=t3,n3=t3*t3*(grad3[gi3]*x3+grad3[gi3+1]*y3+grad3[gi3+2]*z3)}return 32*(n0+n1+n2+n3)},noise4D:function(x,y,z,w){var n0,n1,n2,n3,n4,perm=(this.permMod12,this.perm),grad4=this.grad4,s=(x+y+z+w)*F4,i=Math.floor(x+s),j=Math.floor(y+s),k=Math.floor(z+s),l=Math.floor(w+s),t=(i+j+k+l)*G4,X0=i-t,Y0=j-t,Z0=k-t,W0=l-t,x0=x-X0,y0=y-Y0,z0=z-Z0,w0=w-W0,rankx=0,ranky=0,rankz=0,rankw=0;x0>y0?rankx++:ranky++,x0>z0?rankx++:rankz++,x0>w0?rankx++:rankw++,y0>z0?ranky++:rankz++,y0>w0?ranky++:rankw++,z0>w0?rankz++:rankw++;var i1,j1,k1,l1,i2,j2,k2,l2,i3,j3,k3,l3;i1=rankx>=3?1:0,j1=ranky>=3?1:0,k1=rankz>=3?1:0,l1=rankw>=3?1:0,i2=rankx>=2?1:0,j2=ranky>=2?1:0,k2=rankz>=2?1:0,l2=rankw>=2?1:0,i3=rankx>=1?1:0,j3=ranky>=1?1:0,k3=rankz>=1?1:0,l3=rankw>=1?1:0;var x1=x0-i1+G4,y1=y0-j1+G4,z1=z0-k1+G4,w1=w0-l1+G4,x2=x0-i2+2*G4,y2=y0-j2+2*G4,z2=z0-k2+2*G4,w2=w0-l2+2*G4,x3=x0-i3+3*G4,y3=y0-j3+3*G4,z3=z0-k3+3*G4,w3=w0-l3+3*G4,x4=x0-1+4*G4,y4=y0-1+4*G4,z4=z0-1+4*G4,w4=w0-1+4*G4,ii=255&i,jj=255&j,kk=255&k,ll=255&l,t0=.6-x0*x0-y0*y0-z0*z0-w0*w0;if(0>t0)n0=0;else{var gi0=perm[ii+perm[jj+perm[kk+perm[ll]]]]%32*4;t0*=t0,n0=t0*t0*(grad4[gi0]*x0+grad4[gi0+1]*y0+grad4[gi0+2]*z0+grad4[gi0+3]*w0)}var t1=.6-x1*x1-y1*y1-z1*z1-w1*w1;if(0>t1)n1=0;else{var gi1=perm[ii+i1+perm[jj+j1+perm[kk+k1+perm[ll+l1]]]]%32*4;t1*=t1,n1=t1*t1*(grad4[gi1]*x1+grad4[gi1+1]*y1+grad4[gi1+2]*z1+grad4[gi1+3]*w1)}var t2=.6-x2*x2-y2*y2-z2*z2-w2*w2;if(0>t2)n2=0;else{var gi2=perm[ii+i2+perm[jj+j2+perm[kk+k2+perm[ll+l2]]]]%32*4;t2*=t2,n2=t2*t2*(grad4[gi2]*x2+grad4[gi2+1]*y2+grad4[gi2+2]*z2+grad4[gi2+3]*w2)}var t3=.6-x3*x3-y3*y3-z3*z3-w3*w3;if(0>t3)n3=0;else{var gi3=perm[ii+i3+perm[jj+j3+perm[kk+k3+perm[ll+l3]]]]%32*4;t3*=t3,n3=t3*t3*(grad4[gi3]*x3+grad4[gi3+1]*y3+grad4[gi3+2]*z3+grad4[gi3+3]*w3)}var t4=.6-x4*x4-y4*y4-z4*z4-w4*w4;if(0>t4)n4=0;else{var gi4=perm[ii+1+perm[jj+1+perm[kk+1+perm[ll+1]]]]%32*4;t4*=t4,n4=t4*t4*(grad4[gi4]*x4+grad4[gi4+1]*y4+grad4[gi4+2]*z4+grad4[gi4+3]*w4)}return 27*(n0+n1+n2+n3+n4)}},"undefined"!=typeof define&&define.amd&&define(function(){return SimplexNoise}),"undefined"!=typeof exports?exports.SimplexNoise=SimplexNoise:"undefined"!=typeof window&&(window.SimplexNoise=SimplexNoise),"undefined"!=typeof module&&(module.exports=SimplexNoise)}();
--------------------------------------------------------------------------------
/examples/script-tag-coffee/lib/simplex-noise.min.js:
--------------------------------------------------------------------------------
1 | /*! simplex-noise.js: copyright 2012 Jonas Wagner, licensed under a MIT license. See https://github.com/jwagner/simplex-noise.js for details */
2 | !function(){"use strict";function SimplexNoise(random){random||(random=Math.random),this.p=new Uint8Array(256),this.perm=new Uint8Array(512),this.permMod12=new Uint8Array(512);for(var i=0;256>i;i++)this.p[i]=256*random();for(i=0;512>i;i++)this.perm[i]=this.p[255&i],this.permMod12[i]=this.perm[i]%12}var F2=.5*(Math.sqrt(3)-1),G2=(3-Math.sqrt(3))/6,F3=1/3,G3=1/6,F4=(Math.sqrt(5)-1)/4,G4=(5-Math.sqrt(5))/20;SimplexNoise.prototype={grad3:new Float32Array([1,1,0,-1,1,0,1,-1,0,-1,-1,0,1,0,1,-1,0,1,1,0,-1,-1,0,-1,0,1,1,0,-1,1,0,1,-1,0,-1,-1]),grad4:new Float32Array([0,1,1,1,0,1,1,-1,0,1,-1,1,0,1,-1,-1,0,-1,1,1,0,-1,1,-1,0,-1,-1,1,0,-1,-1,-1,1,0,1,1,1,0,1,-1,1,0,-1,1,1,0,-1,-1,-1,0,1,1,-1,0,1,-1,-1,0,-1,1,-1,0,-1,-1,1,1,0,1,1,1,0,-1,1,-1,0,1,1,-1,0,-1,-1,1,0,1,-1,1,0,-1,-1,-1,0,1,-1,-1,0,-1,1,1,1,0,1,1,-1,0,1,-1,1,0,1,-1,-1,0,-1,1,1,0,-1,1,-1,0,-1,-1,1,0,-1,-1,-1,0]),noise2D:function(xin,yin){var i1,j1,permMod12=this.permMod12,perm=this.perm,grad3=this.grad3,n0=0,n1=0,n2=0,s=(xin+yin)*F2,i=Math.floor(xin+s),j=Math.floor(yin+s),t=(i+j)*G2,X0=i-t,Y0=j-t,x0=xin-X0,y0=yin-Y0;x0>y0?(i1=1,j1=0):(i1=0,j1=1);var x1=x0-i1+G2,y1=y0-j1+G2,x2=x0-1+2*G2,y2=y0-1+2*G2,ii=255&i,jj=255&j,t0=.5-x0*x0-y0*y0;if(t0>=0){var gi0=3*permMod12[ii+perm[jj]];t0*=t0,n0=t0*t0*(grad3[gi0]*x0+grad3[gi0+1]*y0)}var t1=.5-x1*x1-y1*y1;if(t1>=0){var gi1=3*permMod12[ii+i1+perm[jj+j1]];t1*=t1,n1=t1*t1*(grad3[gi1]*x1+grad3[gi1+1]*y1)}var t2=.5-x2*x2-y2*y2;if(t2>=0){var gi2=3*permMod12[ii+1+perm[jj+1]];t2*=t2,n2=t2*t2*(grad3[gi2]*x2+grad3[gi2+1]*y2)}return 70*(n0+n1+n2)},noise3D:function(xin,yin,zin){var n0,n1,n2,n3,i1,j1,k1,i2,j2,k2,permMod12=this.permMod12,perm=this.perm,grad3=this.grad3,s=(xin+yin+zin)*F3,i=Math.floor(xin+s),j=Math.floor(yin+s),k=Math.floor(zin+s),t=(i+j+k)*G3,X0=i-t,Y0=j-t,Z0=k-t,x0=xin-X0,y0=yin-Y0,z0=zin-Z0;x0>=y0?y0>=z0?(i1=1,j1=0,k1=0,i2=1,j2=1,k2=0):x0>=z0?(i1=1,j1=0,k1=0,i2=1,j2=0,k2=1):(i1=0,j1=0,k1=1,i2=1,j2=0,k2=1):z0>y0?(i1=0,j1=0,k1=1,i2=0,j2=1,k2=1):z0>x0?(i1=0,j1=1,k1=0,i2=0,j2=1,k2=1):(i1=0,j1=1,k1=0,i2=1,j2=1,k2=0);var x1=x0-i1+G3,y1=y0-j1+G3,z1=z0-k1+G3,x2=x0-i2+2*G3,y2=y0-j2+2*G3,z2=z0-k2+2*G3,x3=x0-1+3*G3,y3=y0-1+3*G3,z3=z0-1+3*G3,ii=255&i,jj=255&j,kk=255&k,t0=.6-x0*x0-y0*y0-z0*z0;if(0>t0)n0=0;else{var gi0=3*permMod12[ii+perm[jj+perm[kk]]];t0*=t0,n0=t0*t0*(grad3[gi0]*x0+grad3[gi0+1]*y0+grad3[gi0+2]*z0)}var t1=.6-x1*x1-y1*y1-z1*z1;if(0>t1)n1=0;else{var gi1=3*permMod12[ii+i1+perm[jj+j1+perm[kk+k1]]];t1*=t1,n1=t1*t1*(grad3[gi1]*x1+grad3[gi1+1]*y1+grad3[gi1+2]*z1)}var t2=.6-x2*x2-y2*y2-z2*z2;if(0>t2)n2=0;else{var gi2=3*permMod12[ii+i2+perm[jj+j2+perm[kk+k2]]];t2*=t2,n2=t2*t2*(grad3[gi2]*x2+grad3[gi2+1]*y2+grad3[gi2+2]*z2)}var t3=.6-x3*x3-y3*y3-z3*z3;if(0>t3)n3=0;else{var gi3=3*permMod12[ii+1+perm[jj+1+perm[kk+1]]];t3*=t3,n3=t3*t3*(grad3[gi3]*x3+grad3[gi3+1]*y3+grad3[gi3+2]*z3)}return 32*(n0+n1+n2+n3)},noise4D:function(x,y,z,w){var n0,n1,n2,n3,n4,perm=(this.permMod12,this.perm),grad4=this.grad4,s=(x+y+z+w)*F4,i=Math.floor(x+s),j=Math.floor(y+s),k=Math.floor(z+s),l=Math.floor(w+s),t=(i+j+k+l)*G4,X0=i-t,Y0=j-t,Z0=k-t,W0=l-t,x0=x-X0,y0=y-Y0,z0=z-Z0,w0=w-W0,rankx=0,ranky=0,rankz=0,rankw=0;x0>y0?rankx++:ranky++,x0>z0?rankx++:rankz++,x0>w0?rankx++:rankw++,y0>z0?ranky++:rankz++,y0>w0?ranky++:rankw++,z0>w0?rankz++:rankw++;var i1,j1,k1,l1,i2,j2,k2,l2,i3,j3,k3,l3;i1=rankx>=3?1:0,j1=ranky>=3?1:0,k1=rankz>=3?1:0,l1=rankw>=3?1:0,i2=rankx>=2?1:0,j2=ranky>=2?1:0,k2=rankz>=2?1:0,l2=rankw>=2?1:0,i3=rankx>=1?1:0,j3=ranky>=1?1:0,k3=rankz>=1?1:0,l3=rankw>=1?1:0;var x1=x0-i1+G4,y1=y0-j1+G4,z1=z0-k1+G4,w1=w0-l1+G4,x2=x0-i2+2*G4,y2=y0-j2+2*G4,z2=z0-k2+2*G4,w2=w0-l2+2*G4,x3=x0-i3+3*G4,y3=y0-j3+3*G4,z3=z0-k3+3*G4,w3=w0-l3+3*G4,x4=x0-1+4*G4,y4=y0-1+4*G4,z4=z0-1+4*G4,w4=w0-1+4*G4,ii=255&i,jj=255&j,kk=255&k,ll=255&l,t0=.6-x0*x0-y0*y0-z0*z0-w0*w0;if(0>t0)n0=0;else{var gi0=perm[ii+perm[jj+perm[kk+perm[ll]]]]%32*4;t0*=t0,n0=t0*t0*(grad4[gi0]*x0+grad4[gi0+1]*y0+grad4[gi0+2]*z0+grad4[gi0+3]*w0)}var t1=.6-x1*x1-y1*y1-z1*z1-w1*w1;if(0>t1)n1=0;else{var gi1=perm[ii+i1+perm[jj+j1+perm[kk+k1+perm[ll+l1]]]]%32*4;t1*=t1,n1=t1*t1*(grad4[gi1]*x1+grad4[gi1+1]*y1+grad4[gi1+2]*z1+grad4[gi1+3]*w1)}var t2=.6-x2*x2-y2*y2-z2*z2-w2*w2;if(0>t2)n2=0;else{var gi2=perm[ii+i2+perm[jj+j2+perm[kk+k2+perm[ll+l2]]]]%32*4;t2*=t2,n2=t2*t2*(grad4[gi2]*x2+grad4[gi2+1]*y2+grad4[gi2+2]*z2+grad4[gi2+3]*w2)}var t3=.6-x3*x3-y3*y3-z3*z3-w3*w3;if(0>t3)n3=0;else{var gi3=perm[ii+i3+perm[jj+j3+perm[kk+k3+perm[ll+l3]]]]%32*4;t3*=t3,n3=t3*t3*(grad4[gi3]*x3+grad4[gi3+1]*y3+grad4[gi3+2]*z3+grad4[gi3+3]*w3)}var t4=.6-x4*x4-y4*y4-z4*z4-w4*w4;if(0>t4)n4=0;else{var gi4=perm[ii+1+perm[jj+1+perm[kk+1+perm[ll+1]]]]%32*4;t4*=t4,n4=t4*t4*(grad4[gi4]*x4+grad4[gi4+1]*y4+grad4[gi4+2]*z4+grad4[gi4+3]*w4)}return 27*(n0+n1+n2+n3+n4)}},"undefined"!=typeof define&&define.amd&&define(function(){return SimplexNoise}),"undefined"!=typeof exports?exports.SimplexNoise=SimplexNoise:"undefined"!=typeof window&&(window.SimplexNoise=SimplexNoise),"undefined"!=typeof module&&(module.exports=SimplexNoise)}();
--------------------------------------------------------------------------------
/examples/webpack-coffee/lib/simplex-noise.min.js:
--------------------------------------------------------------------------------
1 | /*! simplex-noise.js: copyright 2012 Jonas Wagner, licensed under a MIT license. See https://github.com/jwagner/simplex-noise.js for details */
2 | !function(){"use strict";function SimplexNoise(random){random||(random=Math.random),this.p=new Uint8Array(256),this.perm=new Uint8Array(512),this.permMod12=new Uint8Array(512);for(var i=0;256>i;i++)this.p[i]=256*random();for(i=0;512>i;i++)this.perm[i]=this.p[255&i],this.permMod12[i]=this.perm[i]%12}var F2=.5*(Math.sqrt(3)-1),G2=(3-Math.sqrt(3))/6,F3=1/3,G3=1/6,F4=(Math.sqrt(5)-1)/4,G4=(5-Math.sqrt(5))/20;SimplexNoise.prototype={grad3:new Float32Array([1,1,0,-1,1,0,1,-1,0,-1,-1,0,1,0,1,-1,0,1,1,0,-1,-1,0,-1,0,1,1,0,-1,1,0,1,-1,0,-1,-1]),grad4:new Float32Array([0,1,1,1,0,1,1,-1,0,1,-1,1,0,1,-1,-1,0,-1,1,1,0,-1,1,-1,0,-1,-1,1,0,-1,-1,-1,1,0,1,1,1,0,1,-1,1,0,-1,1,1,0,-1,-1,-1,0,1,1,-1,0,1,-1,-1,0,-1,1,-1,0,-1,-1,1,1,0,1,1,1,0,-1,1,-1,0,1,1,-1,0,-1,-1,1,0,1,-1,1,0,-1,-1,-1,0,1,-1,-1,0,-1,1,1,1,0,1,1,-1,0,1,-1,1,0,1,-1,-1,0,-1,1,1,0,-1,1,-1,0,-1,-1,1,0,-1,-1,-1,0]),noise2D:function(xin,yin){var i1,j1,permMod12=this.permMod12,perm=this.perm,grad3=this.grad3,n0=0,n1=0,n2=0,s=(xin+yin)*F2,i=Math.floor(xin+s),j=Math.floor(yin+s),t=(i+j)*G2,X0=i-t,Y0=j-t,x0=xin-X0,y0=yin-Y0;x0>y0?(i1=1,j1=0):(i1=0,j1=1);var x1=x0-i1+G2,y1=y0-j1+G2,x2=x0-1+2*G2,y2=y0-1+2*G2,ii=255&i,jj=255&j,t0=.5-x0*x0-y0*y0;if(t0>=0){var gi0=3*permMod12[ii+perm[jj]];t0*=t0,n0=t0*t0*(grad3[gi0]*x0+grad3[gi0+1]*y0)}var t1=.5-x1*x1-y1*y1;if(t1>=0){var gi1=3*permMod12[ii+i1+perm[jj+j1]];t1*=t1,n1=t1*t1*(grad3[gi1]*x1+grad3[gi1+1]*y1)}var t2=.5-x2*x2-y2*y2;if(t2>=0){var gi2=3*permMod12[ii+1+perm[jj+1]];t2*=t2,n2=t2*t2*(grad3[gi2]*x2+grad3[gi2+1]*y2)}return 70*(n0+n1+n2)},noise3D:function(xin,yin,zin){var n0,n1,n2,n3,i1,j1,k1,i2,j2,k2,permMod12=this.permMod12,perm=this.perm,grad3=this.grad3,s=(xin+yin+zin)*F3,i=Math.floor(xin+s),j=Math.floor(yin+s),k=Math.floor(zin+s),t=(i+j+k)*G3,X0=i-t,Y0=j-t,Z0=k-t,x0=xin-X0,y0=yin-Y0,z0=zin-Z0;x0>=y0?y0>=z0?(i1=1,j1=0,k1=0,i2=1,j2=1,k2=0):x0>=z0?(i1=1,j1=0,k1=0,i2=1,j2=0,k2=1):(i1=0,j1=0,k1=1,i2=1,j2=0,k2=1):z0>y0?(i1=0,j1=0,k1=1,i2=0,j2=1,k2=1):z0>x0?(i1=0,j1=1,k1=0,i2=0,j2=1,k2=1):(i1=0,j1=1,k1=0,i2=1,j2=1,k2=0);var x1=x0-i1+G3,y1=y0-j1+G3,z1=z0-k1+G3,x2=x0-i2+2*G3,y2=y0-j2+2*G3,z2=z0-k2+2*G3,x3=x0-1+3*G3,y3=y0-1+3*G3,z3=z0-1+3*G3,ii=255&i,jj=255&j,kk=255&k,t0=.6-x0*x0-y0*y0-z0*z0;if(0>t0)n0=0;else{var gi0=3*permMod12[ii+perm[jj+perm[kk]]];t0*=t0,n0=t0*t0*(grad3[gi0]*x0+grad3[gi0+1]*y0+grad3[gi0+2]*z0)}var t1=.6-x1*x1-y1*y1-z1*z1;if(0>t1)n1=0;else{var gi1=3*permMod12[ii+i1+perm[jj+j1+perm[kk+k1]]];t1*=t1,n1=t1*t1*(grad3[gi1]*x1+grad3[gi1+1]*y1+grad3[gi1+2]*z1)}var t2=.6-x2*x2-y2*y2-z2*z2;if(0>t2)n2=0;else{var gi2=3*permMod12[ii+i2+perm[jj+j2+perm[kk+k2]]];t2*=t2,n2=t2*t2*(grad3[gi2]*x2+grad3[gi2+1]*y2+grad3[gi2+2]*z2)}var t3=.6-x3*x3-y3*y3-z3*z3;if(0>t3)n3=0;else{var gi3=3*permMod12[ii+1+perm[jj+1+perm[kk+1]]];t3*=t3,n3=t3*t3*(grad3[gi3]*x3+grad3[gi3+1]*y3+grad3[gi3+2]*z3)}return 32*(n0+n1+n2+n3)},noise4D:function(x,y,z,w){var n0,n1,n2,n3,n4,perm=(this.permMod12,this.perm),grad4=this.grad4,s=(x+y+z+w)*F4,i=Math.floor(x+s),j=Math.floor(y+s),k=Math.floor(z+s),l=Math.floor(w+s),t=(i+j+k+l)*G4,X0=i-t,Y0=j-t,Z0=k-t,W0=l-t,x0=x-X0,y0=y-Y0,z0=z-Z0,w0=w-W0,rankx=0,ranky=0,rankz=0,rankw=0;x0>y0?rankx++:ranky++,x0>z0?rankx++:rankz++,x0>w0?rankx++:rankw++,y0>z0?ranky++:rankz++,y0>w0?ranky++:rankw++,z0>w0?rankz++:rankw++;var i1,j1,k1,l1,i2,j2,k2,l2,i3,j3,k3,l3;i1=rankx>=3?1:0,j1=ranky>=3?1:0,k1=rankz>=3?1:0,l1=rankw>=3?1:0,i2=rankx>=2?1:0,j2=ranky>=2?1:0,k2=rankz>=2?1:0,l2=rankw>=2?1:0,i3=rankx>=1?1:0,j3=ranky>=1?1:0,k3=rankz>=1?1:0,l3=rankw>=1?1:0;var x1=x0-i1+G4,y1=y0-j1+G4,z1=z0-k1+G4,w1=w0-l1+G4,x2=x0-i2+2*G4,y2=y0-j2+2*G4,z2=z0-k2+2*G4,w2=w0-l2+2*G4,x3=x0-i3+3*G4,y3=y0-j3+3*G4,z3=z0-k3+3*G4,w3=w0-l3+3*G4,x4=x0-1+4*G4,y4=y0-1+4*G4,z4=z0-1+4*G4,w4=w0-1+4*G4,ii=255&i,jj=255&j,kk=255&k,ll=255&l,t0=.6-x0*x0-y0*y0-z0*z0-w0*w0;if(0>t0)n0=0;else{var gi0=perm[ii+perm[jj+perm[kk+perm[ll]]]]%32*4;t0*=t0,n0=t0*t0*(grad4[gi0]*x0+grad4[gi0+1]*y0+grad4[gi0+2]*z0+grad4[gi0+3]*w0)}var t1=.6-x1*x1-y1*y1-z1*z1-w1*w1;if(0>t1)n1=0;else{var gi1=perm[ii+i1+perm[jj+j1+perm[kk+k1+perm[ll+l1]]]]%32*4;t1*=t1,n1=t1*t1*(grad4[gi1]*x1+grad4[gi1+1]*y1+grad4[gi1+2]*z1+grad4[gi1+3]*w1)}var t2=.6-x2*x2-y2*y2-z2*z2-w2*w2;if(0>t2)n2=0;else{var gi2=perm[ii+i2+perm[jj+j2+perm[kk+k2+perm[ll+l2]]]]%32*4;t2*=t2,n2=t2*t2*(grad4[gi2]*x2+grad4[gi2+1]*y2+grad4[gi2+2]*z2+grad4[gi2+3]*w2)}var t3=.6-x3*x3-y3*y3-z3*z3-w3*w3;if(0>t3)n3=0;else{var gi3=perm[ii+i3+perm[jj+j3+perm[kk+k3+perm[ll+l3]]]]%32*4;t3*=t3,n3=t3*t3*(grad4[gi3]*x3+grad4[gi3+1]*y3+grad4[gi3+2]*z3+grad4[gi3+3]*w3)}var t4=.6-x4*x4-y4*y4-z4*z4-w4*w4;if(0>t4)n4=0;else{var gi4=perm[ii+1+perm[jj+1+perm[kk+1+perm[ll+1]]]]%32*4;t4*=t4,n4=t4*t4*(grad4[gi4]*x4+grad4[gi4+1]*y4+grad4[gi4+2]*z4+grad4[gi4+3]*w4)}return 27*(n0+n1+n2+n3+n4)}},"undefined"!=typeof define&&define.amd&&define(function(){return SimplexNoise}),"undefined"!=typeof exports?exports.SimplexNoise=SimplexNoise:"undefined"!=typeof window&&(window.SimplexNoise=SimplexNoise),"undefined"!=typeof module&&(module.exports=SimplexNoise)}();
--------------------------------------------------------------------------------
/source/styles.css:
--------------------------------------------------------------------------------
1 | .editor {
2 | -webkit-touch-callout: none;
3 | -webkit-user-select: none;
4 | -moz-user-select: none;
5 | user-select: none;
6 | position: absolute;
7 | top: 0;
8 | left: 0;
9 | right: 0;
10 | bottom: 0;
11 | display: flex;
12 | /* This is to allow clicking through to the canvas which is underneath the editor.
13 | It has to be carefully re-enabled on children with pointer-events: auto; */
14 | pointer-events: none;
15 | }
16 | .grabbable {
17 | cursor: move; /* fallback if grab cursor is unsupported */
18 | cursor: grab;
19 | cursor: -moz-grab;
20 | cursor: -webkit-grab;
21 | }
22 | /* Apply a "closed-hand" cursor during drag operation. */
23 | .grabbable:active {
24 | cursor: grabbing;
25 | cursor: -moz-grabbing;
26 | cursor: -webkit-grabbing;
27 | }
28 | /* Sidebars */
29 | .bar {
30 | background: white;
31 | transition: opacity 0.2s ease;
32 | display: flex;
33 | align-items: stretch;
34 | align-content: flex-start;
35 | flex: 0 0 auto;
36 | pointer-events: auto;
37 | }
38 | .bar:not(.visible) {
39 | opacity: 0;
40 | pointer-events: none;
41 | }
42 | .sidebar {
43 | position: absolute;
44 | z-index: 1;
45 | top: 0;
46 | height: 100%;
47 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.4);
48 | flex-direction: column;
49 | }
50 | .sidebar:not(.right-sidebar) {
51 | left: 0;
52 | }
53 | .sidebar.right-sidebar {
54 | right: 0;
55 | }
56 | .tools-bar:not(.visible) {
57 | /* It needs to not take up vertical space when hidden.
58 | The opacity transition is bad with height: 0 here,
59 | so just hide the element until a better transition is developed. */
60 | /* height: 0; */
61 | display: none;
62 | }
63 | .tools-bar {
64 | z-index: 2;
65 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.4);
66 | flex-direction: row;
67 | align-items: center;
68 | }
69 | .layout-horizontal {
70 | display: flex;
71 | flex-direction: row;
72 | flex: 1;
73 | }
74 | .layout-vertical {
75 | display: flex;
76 | flex-direction: column;
77 | flex: 1;
78 | }
79 | .bar article {
80 | padding: 1rem;
81 | padding-top: 0.5rem;
82 | display: flex;
83 | flex-direction: column;
84 | }
85 | .tools button[aria-pressed="true"] {
86 | background: rgba(0, 157, 255, 0.12);
87 | box-shadow: 0 0 0 2px rgba(0, 157, 255, 0.5) inset;
88 | }
89 | .bar button img {
90 | pointer-events: none;
91 | }
92 | .tools button:disabled img {
93 | opacity: 0.4;
94 | }
95 | .mdl-button--icon {
96 | display: inline-flex;
97 | align-items: center;
98 | justify-content: center;
99 | }
100 | .tool-options label {
101 | display: inline-flex;
102 | margin-left: 2rem;
103 | }
104 | .tool-options label span {
105 | margin-right: -20px;
106 | }
107 | .bar article:hover {
108 | background: rgba(0, 0, 0, 0.08);
109 | }
110 | .bar article:active,
111 | .bar article.selected {
112 | background: rgba(0, 0, 0, 0.12);
113 | }
114 | .bar article canvas {
115 | background: rgba(50, 200, 255, 0.7);
116 | }
117 | .bar article:hover canvas,
118 | .bar article:active canvas,
119 | .bar article.selected canvas {
120 | background: rgba(50, 200, 255, 1);
121 | }
122 | .entity-preview {
123 | position: relative;
124 | }
125 | .entity-preview .error {
126 | position: absolute;
127 | top: 0;
128 | left: 0;
129 | width: 100%;
130 | height: 100%;
131 | background: rgba(255, 0, 0, 0.5);
132 | color: white;
133 | font-family: monospace;
134 | text-align: center;
135 | }
136 | .bar h1 {
137 | text-align: center;
138 | font-size: 2em;
139 | font-weight: normal;
140 | margin: 0.1em 0;
141 | }
142 | .bar article > h1 {
143 | pointer-events: none;
144 | }
145 | .bar article .title-bar {
146 | display: flex;
147 | flex-direction: row;
148 | }
149 | .bar .name {
150 | font-size: 1.2em;
151 | font-weight: normal;
152 | font-family: sans-serif;
153 | margin: 0;
154 | margin-bottom: 0.1em;
155 | }
156 | .entities-bar .name {
157 | text-align: center;
158 | }
159 | .bar article .mdl-textfield {
160 | width: auto;
161 | padding: 0;
162 | padding-bottom: 0.3rem;
163 | }
164 | button,
165 | canvas,
166 | img,
167 | article, /* representing entities, poses, animations, animation frames - things with EntityPreviews in them */
168 | .anims > * { /* includes headings and .anim-groups */
169 | flex: 0 0 auto;
170 | }
171 | .anim-bar {
172 | flex-direction: row;
173 | align-items: flex-start;
174 | }
175 | .anim-bar > * {
176 | height: 100%;
177 | }
178 | /* TODO: refactor bars and subbars */
179 | .anim-bar > *:not(:first-child) {
180 | border-left: 1px solid rgba(0, 0, 0, 0.12);
181 | }
182 | .anims,
183 | .anim-group {
184 | display: flex;
185 | flex-direction: column;
186 | align-items: stretch;
187 | }
188 | .anims,
189 | .animation-frames,
190 | .entities-bar {
191 | overflow-y: auto;
192 | overflow-x: hidden;
193 | }
194 | /* TODO: refactor bars and subbars */
195 | .animation-frames {
196 | /*transition: 0.1s ease;*/
197 | }
198 | .animation-frames:not(.visible) {
199 | opacity: 0;
200 | pointer-events: none;
201 | width: 0;
202 | /*transform: translate(-100%, 0);*/
203 | }
204 | .add-anim-fab {
205 | margin: 0.5rem 0 !important;
206 | align-self: center;
207 | }
208 | .poses,
209 | .animations {
210 | width: 100%;
211 | }
212 | article.placeholder {
213 | padding: 2rem;
214 | text-align: center;
215 | background: rgba(128, 59, 110, 0.16);
216 | color: rgba(0, 0, 0, 0.5);
217 | font-size: 1.4em;
218 | pointer-events: none;
219 | }
220 |
221 | .warning {
222 | position: absolute;
223 | top: 0;
224 | right: 0;
225 | z-index: 50;
226 | margin: 15px;
227 | padding: 15px;
228 | background: #FFF9C4;
229 | color: #BF360C;
230 | border-radius: 2px;
231 | transition: opacity 0.2s ease;
232 | pointer-events: auto;
233 | }
234 | .warning:not(.show) {
235 | pointer-events: none;
236 | opacity: 0;
237 | }
238 |
239 | .mdl-textfield__input:invalid {
240 | background: rgba(255, 0, 0, 0.1);
241 | border-bottom-color: red;
242 | }
243 |
--------------------------------------------------------------------------------
/source/base-entities/Entity.coffee:
--------------------------------------------------------------------------------
1 |
2 | fs = window.require? "fs"
3 | path = window.require? "path"
4 |
5 | import Pose from "../structure/Pose.coffee"
6 | import BoneStructure from "../structure/BoneStructure.coffee"
7 | import {entityClasses} from "../entity-class-registry.coffee"
8 |
9 | export default class Entity
10 | constructor: ->
11 | @structure = new BoneStructure
12 | @x = 0
13 | @y = 0
14 | @id = uuid()
15 |
16 | @bbox_padding = 2
17 | # TODO: depth system
18 | # @drawing_pieces = {}
19 |
20 | @_class_ = @constructor.name
21 |
22 | @initAnimation: (EntityClass)->
23 | EntityClass.poses = {}
24 | EntityClass.animations = {}
25 | EntityClass.animation_json_path = "./animations/#{EntityClass.name}.json"
26 | Entity.loadAnimations(EntityClass)
27 |
28 | @loadAnimations: (EntityClass)->
29 | animationsFromJSON = ({poses, animations})->
30 | EntityClass.poses = {}
31 | EntityClass.animations = {}
32 | for pose_name, pose of poses
33 | EntityClass.poses[pose_name] = new Pose(pose)
34 | for animation_name, animation of animations
35 | EntityClass.animations[animation_name] = (new Pose(pose) for pose in animation)
36 |
37 | if fs?
38 | try
39 | json = fs.readFileSync(EntityClass.animation_json_path)
40 | catch e
41 | throw e unless e.code is "ENOENT"
42 | else
43 | json = localStorage["Skele2D #{EntityClass.name} animations"]
44 | if json
45 | animationsFromJSON(JSON.parse(json)) if json
46 | else
47 | req = new XMLHttpRequest
48 | req.addEventListener "load", (e)=>
49 | json = req.responseText
50 | animationsFromJSON(JSON.parse(json)) if json
51 | req.open("GET", EntityClass.animation_json_path)
52 | req.send()
53 |
54 | @saveAnimations: (EntityClass)->
55 | {poses, animations} = EntityClass
56 | json = JSON.stringify({poses, animations}, null, "\t")
57 | if fs?
58 | try
59 | fs.mkdirSync(path.dirname(EntityClass.animation_json_path))
60 | catch e
61 | throw e unless e.code is "EEXIST"
62 | fs.writeFileSync(EntityClass.animation_json_path, json)
63 | else
64 | localStorage["Skele2D #{EntityClass.name} animations"] = json
65 |
66 | @fromJSON: (def)->
67 | unless typeof def._class_ is "string"
68 | console.error "Erroneous entity definition:", def
69 | throw new Error "Expected entity to have a string _class_, _class_ is #{def._class_}"
70 | unless entityClasses[def._class_]
71 | throw new Error "Entity class '#{def._class_}' does not exist"
72 | entity = new entityClasses[def._class_]
73 | entity.fromJSON(def)
74 | entity
75 |
76 | fromJSON: (def)->
77 | if def._class_ isnt @_class_
78 | throw new Error "Tried to initialize #{@_class_} entity from JSON with _class_ #{JSON.stringify(def._class_)}"
79 | for k, v of def when k isnt "_class_"
80 | if @[k]?.fromJSON
81 | @[k].fromJSON(v)
82 | else
83 | @[k] = v
84 |
85 | resolveReferences: (world)->
86 | if @_refs_
87 | for k, id of @_refs_
88 | @[k] = world.getEntityByID(id)
89 | delete @_refs_
90 |
91 | toJSON: ->
92 | obj = {}
93 | for k, v of @ when k isnt "_refs_"
94 | if v instanceof Entity
95 | obj._refs_ ?= {}
96 | obj._refs_[k] = v.id
97 | else
98 | obj[k] = v
99 | obj
100 |
101 | toWorld: (point)->
102 | x: point.x + @x
103 | y: point.y + @y
104 |
105 | fromWorld: (point)->
106 | x: point.x - @x
107 | y: point.y - @y
108 |
109 | bbox: ->
110 | min_point = {x: +Infinity, y: +Infinity}
111 | max_point = {x: -Infinity, y: -Infinity}
112 | for point_name, point of @structure.points
113 | min_point.x = Math.min(min_point.x, point.x)
114 | min_point.y = Math.min(min_point.y, point.y)
115 | max_point.x = Math.max(max_point.x, point.x)
116 | max_point.y = Math.max(max_point.y, point.y)
117 | min_point.x = 0 unless isFinite(min_point.x)
118 | min_point.y = 0 unless isFinite(min_point.y)
119 | max_point.x = 0 unless isFinite(max_point.x)
120 | max_point.y = 0 unless isFinite(max_point.y)
121 | min_point.x -= @bbox_padding
122 | min_point.y -= @bbox_padding
123 | max_point.x += @bbox_padding
124 | max_point.y += @bbox_padding
125 | min_point_in_world = @toWorld(min_point)
126 | max_point_in_world = @toWorld(max_point)
127 | x: min_point_in_world.x
128 | y: min_point_in_world.y
129 | width: max_point_in_world.x - min_point_in_world.x
130 | height: max_point_in_world.y - min_point_in_world.y
131 |
132 | # animate: ()->
133 | # @structure.setPose(Pose.lerp(various_poses))
134 |
135 | initLayout: ->
136 | EntityClass = @constructor
137 | if EntityClass.poses
138 | default_pose = EntityClass.poses["Default"] ? EntityClass.poses["Stand"] ? EntityClass.poses["Standing"] ? EntityClass.poses["Idle"]
139 | if default_pose
140 | @structure.setPose(default_pose)
141 | return
142 | ys = {}
143 | y = 0
144 | for point_name, point of @structure.points
145 | side = point_name.match(/left|right/)?[0]
146 | if side
147 | sideless_point_name = point_name.replace(/left|right/, "")
148 | if ys[sideless_point_name]
149 | y = ys[sideless_point_name]
150 | else
151 | y += 10
152 | ys[sideless_point_name] = y
153 | if side is "left"
154 | point.x = -5.5
155 | if side is "right"
156 | point.x = +5.5
157 | point.x *= 0.7 if point_name.match(/lower/)
158 | point.y = y
159 |
160 | for [0..2000]
161 | @structure.stepLayout(center: yes, repel: yes)
162 | for [0..4000]
163 | @structure.stepLayout()
164 |
165 | step: (world)->
166 | draw: (ctx)->
167 |
168 | # TODO: function to call into the depth system
169 | # drawStructure: (drawing_functions)->
170 | # for point_name, fn of drawing_functions.points
171 | # fn(@structure.points[point_name])
172 | # for segment_name, fn of drawing_functions.segments
173 | # fn(@structure.segments[segment_name])
174 |
--------------------------------------------------------------------------------
/source/components/AnimGroup.coffee:
--------------------------------------------------------------------------------
1 | import {Component} from "react"
2 | import ReactDOM from "react-dom"
3 | import E from "react-script"
4 | import Anim from "./Anim.coffee"
5 | import Pose from "../structure/Pose.coffee"
6 | import Entity from "../base-entities/Entity.coffee"
7 | import plusIcon from "../icons/plus.svg"
8 |
9 | export default class AnimGroup extends Component
10 | render: ->
11 | {entity, EntityClass, array_to_push_anims_to, update, type_of_anims, editor} = @props
12 | E ".anim-group",
13 | if EntityClass?
14 | if type_of_anims is "poses"
15 | if EntityClass.poses?
16 | if Object.keys(EntityClass.poses).length > 0
17 | i = 0
18 | for pose_name, pose of EntityClass.poses then do (pose_name, pose)=>
19 | i += 1
20 | selected = editor.editing_entity_anim_name is pose_name and not editor.editing_entity_animation_frame_index?
21 | E Anim, {
22 | key: i
23 | name: pose_name
24 | entity, EntityClass, selected, editor, update, type_of_anims
25 | # pose
26 | select: =>
27 | editor.editing_entity_anim_name = pose_name
28 | editor.editing_entity_animation_frame_index = null
29 | unless pose_name is "Current Pose"
30 | entity.structure.setPose(EntityClass.poses[pose_name])
31 | delete_item: =>
32 | delete EntityClass.poses[pose_name]
33 | editor.editing_entity_anim_name = "Current Pose"
34 | editor.editing_entity_animation_frame_index = null
35 | get_pose: =>
36 | if pose_name is "Current Pose" or selected
37 | entity.structure.getPose()
38 | else
39 | EntityClass.poses[pose_name]
40 | ref: (anim)=>
41 | array_to_push_anims_to.push(anim) if anim?
42 | }
43 | else
44 | E "article.placeholder", key: "placeholder", "No poses"
45 | else
46 | E "article.placeholder", key: "placeholder", "Entity class is not initialized for animation"
47 | else if type_of_anims is "animations"
48 | if EntityClass.animations?
49 | if Object.keys(EntityClass.animations).length > 0
50 | i = 0
51 | for animation_name, animation of EntityClass.animations then do (animation_name, animation)=>
52 | i += 1
53 | selected = editor.editing_entity_anim_name is animation_name and editor.editing_entity_animation_frame_index?
54 | E Anim, {
55 | key: i
56 | name: animation_name
57 | entity, EntityClass, selected, editor, update, type_of_anims
58 | # animation
59 | # TODO: bounds of anim should be determined across all frames
60 | select: =>
61 | editor.editing_entity_anim_name = animation_name
62 | editor.editing_entity_animation_frame_index = 0
63 | pose = EntityClass.animations[animation_name]?[0]
64 | entity.structure.setPose(pose) if pose
65 | delete_item: =>
66 | delete EntityClass.animations[animation_name]
67 | editor.editing_entity_anim_name = "Current Pose"
68 | editor.editing_entity_animation_frame_index = null
69 | get_pose: =>
70 | # TODO: animate only if anim is the hovered||selected one
71 | animation = EntityClass.animations[animation_name]
72 | return unless animation # TODO: shouldn't need this or other ?s
73 | Pose.lerpAnimationLoop(animation, EntityClass.animations[animation_name].length * Date.now()/1000/2)
74 | ref: (anim)=>
75 | array_to_push_anims_to.push(anim) if anim?
76 | }
77 | else
78 | E "article.placeholder", key: "placeholder", "No animations"
79 | else
80 | E "article.placeholder", key: "placeholder", "Entity class is not initialized for animation"
81 | else if type_of_anims is "animation-frames"
82 | if EntityClass.animations?
83 | animation_name = editor.editing_entity_anim_name
84 | frames = EntityClass.animations[animation_name]
85 | if frames?
86 | for frame, frame_index in frames then do (frame, frame_index)=>
87 | selected = editor.editing_entity_anim_name is animation_name and editor.editing_entity_animation_frame_index is frame_index
88 | E Anim, {
89 | key: frame_index
90 | name: "Frame #{frame_index}"
91 | entity, EntityClass, selected, editor, update, type_of_anims
92 | # animation frame
93 | select: =>
94 | editor.editing_entity_anim_name = animation_name
95 | editor.editing_entity_animation_frame_index = frame_index
96 | pose = EntityClass.animations[animation_name][frame_index]
97 | entity.structure.setPose(pose)
98 | delete_item: =>
99 | EntityClass.animations[animation_name].splice(frame_index, 1)
100 | get_pose: =>
101 | if selected
102 | entity.structure.getPose()
103 | else
104 | animation = EntityClass.animations[animation_name]
105 | animation?[frame_index]
106 | ref: (anim)=>
107 | array_to_push_anims_to.push(anim) if anim?
108 | }
109 | else
110 | E "article.placeholder", key: "placeholder", "Error: Trying to display the frames of a non-existent animation"
111 | else
112 | E "article.placeholder", key: "placeholder", "Error: Entity class is not initialized for animation, trying to display the frames of an animation?"
113 | else
114 | E "article.placeholder", key: "placeholder", "Error: weird type_of_anims for AnimGroup #{type_of_anims}"
115 | E "button.add-anim-fab.mdl-button.mdl-js-button.mdl-button--fab.mdl-js-ripple-effect.mdl-button--colored",
116 | key: "add-button"
117 | ref: (@new_anim_button)=>
118 | onClick: =>
119 | if type_of_anims is "animation-frames"
120 | animation = EntityClass.animations[editor.editing_entity_anim_name]
121 | new_pose = entity.structure.getPose()
122 | animation.push(new_pose)
123 | editor.editing_entity_animation_frame_index = animation.length - 1
124 | else
125 | default_name = switch type_of_anims
126 | when "poses" then "New Pose"
127 | when "animations" then "New Animation"
128 | new_name = default_name
129 | i = 1
130 | while EntityClass[type_of_anims][new_name]?
131 | new_name = "#{default_name} #{i}"
132 | i += 1
133 |
134 | switch type_of_anims
135 | when "poses"
136 | EntityClass.poses[new_name] = entity.structure.getPose()
137 | editor.editing_entity_animation_frame_index = null
138 | when "animations"
139 | EntityClass.animations[new_name] = [entity.structure.getPose()]
140 | editor.editing_entity_animation_frame_index = 0
141 |
142 | editor.editing_entity_anim_name = new_name
143 |
144 | Entity.saveAnimations(EntityClass)
145 |
146 | update()
147 |
148 | # E "i.material-icons", "add"
149 | E "img", src: plusIcon, style: {filter: "invert()"}
150 |
151 | componentDidMount: =>
152 | componentHandler.upgradeElement(ReactDOM.findDOMNode(@new_anim_button))
153 | # XXX: have to upgrade when the bar becomes visible
154 | componentDidUpdate: =>
155 | componentHandler.upgradeElement(ReactDOM.findDOMNode(@new_anim_button))
156 |
--------------------------------------------------------------------------------
/examples/esm/lib/uuid.js:
--------------------------------------------------------------------------------
1 | // uuid.js
2 | //
3 | // Copyright (c) 2010-2012 Robert Kieffer
4 | // MIT License - http://opensource.org/licenses/mit-license.php
5 |
6 | /*global window, require, define */
7 | (function(_window) {
8 | 'use strict';
9 |
10 | // Unique ID creation requires a high quality random # generator. We feature
11 | // detect to determine the best RNG source, normalizing to a function that
12 | // returns 128-bits of randomness, since that's what's usually required
13 | var _rng, _mathRNG, _nodeRNG, _whatwgRNG, _previousRoot;
14 |
15 | function setupBrowser() {
16 | // Allow for MSIE11 msCrypto
17 | var _crypto = _window.crypto || _window.msCrypto;
18 |
19 | if (!_rng && _crypto && _crypto.getRandomValues) {
20 | // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto
21 | //
22 | // Moderately fast, high quality
23 | try {
24 | var _rnds8 = new Uint8Array(16);
25 | _whatwgRNG = _rng = function whatwgRNG() {
26 | _crypto.getRandomValues(_rnds8);
27 | return _rnds8;
28 | };
29 | _rng();
30 | } catch(e) {}
31 | }
32 |
33 | if (!_rng) {
34 | // Math.random()-based (RNG)
35 | //
36 | // If all else fails, use Math.random(). It's fast, but is of unspecified
37 | // quality.
38 | var _rnds = new Array(16);
39 | _mathRNG = _rng = function() {
40 | for (var i = 0, r; i < 16; i++) {
41 | if ((i & 0x03) === 0) { r = Math.random() * 0x100000000; }
42 | _rnds[i] = r >>> ((i & 0x03) << 3) & 0xff;
43 | }
44 |
45 | return _rnds;
46 | };
47 | if ('undefined' !== typeof console && console.warn) {
48 | console.warn("[SECURITY] node-uuid: crypto not usable, falling back to insecure Math.random()");
49 | }
50 | }
51 | }
52 |
53 | function setupNode() {
54 | // Node.js crypto-based RNG - http://nodejs.org/docs/v0.6.2/api/crypto.html
55 | //
56 | // Moderately fast, high quality
57 | if ('function' === typeof require) {
58 | try {
59 | var _rb = require('crypto').randomBytes;
60 | _nodeRNG = _rng = _rb && function() {return _rb(16);};
61 | _rng();
62 | } catch(e) {}
63 | }
64 | }
65 |
66 | if (_window) {
67 | setupBrowser();
68 | } else {
69 | setupNode();
70 | }
71 |
72 | // Buffer class to use
73 | var BufferClass = ('function' === typeof Buffer) ? Buffer : Array;
74 |
75 | // Maps for number <-> hex string conversion
76 | var _byteToHex = [];
77 | var _hexToByte = {};
78 | for (var i = 0; i < 256; i++) {
79 | _byteToHex[i] = (i + 0x100).toString(16).substr(1);
80 | _hexToByte[_byteToHex[i]] = i;
81 | }
82 |
83 | // **`parse()` - Parse a UUID into it's component bytes**
84 | function parse(s, buf, offset) {
85 | var i = (buf && offset) || 0, ii = 0;
86 |
87 | buf = buf || [];
88 | s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) {
89 | if (ii < 16) { // Don't overflow!
90 | buf[i + ii++] = _hexToByte[oct];
91 | }
92 | });
93 |
94 | // Zero out remaining bytes if string was short
95 | while (ii < 16) {
96 | buf[i + ii++] = 0;
97 | }
98 |
99 | return buf;
100 | }
101 |
102 | // **`unparse()` - Convert UUID byte array (ala parse()) into a string**
103 | function unparse(buf, offset) {
104 | var i = offset || 0, bth = _byteToHex;
105 | return bth[buf[i++]] + bth[buf[i++]] +
106 | bth[buf[i++]] + bth[buf[i++]] + '-' +
107 | bth[buf[i++]] + bth[buf[i++]] + '-' +
108 | bth[buf[i++]] + bth[buf[i++]] + '-' +
109 | bth[buf[i++]] + bth[buf[i++]] + '-' +
110 | bth[buf[i++]] + bth[buf[i++]] +
111 | bth[buf[i++]] + bth[buf[i++]] +
112 | bth[buf[i++]] + bth[buf[i++]];
113 | }
114 |
115 | // **`v1()` - Generate time-based UUID**
116 | //
117 | // Inspired by https://github.com/LiosK/UUID.js
118 | // and http://docs.python.org/library/uuid.html
119 |
120 | // random #'s we need to init node and clockseq
121 | var _seedBytes = _rng();
122 |
123 | // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
124 | var _nodeId = [
125 | _seedBytes[0] | 0x01,
126 | _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]
127 | ];
128 |
129 | // Per 4.2.2, randomize (14 bit) clockseq
130 | var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff;
131 |
132 | // Previous uuid creation time
133 | var _lastMSecs = 0, _lastNSecs = 0;
134 |
135 | // See https://github.com/broofa/node-uuid for API details
136 | function v1(options, buf, offset) {
137 | var i = buf && offset || 0;
138 | var b = buf || [];
139 |
140 | options = options || {};
141 |
142 | var clockseq = (options.clockseq != null) ? options.clockseq : _clockseq;
143 |
144 | // UUID timestamps are 100 nano-second units since the Gregorian epoch,
145 | // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so
146 | // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
147 | // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
148 | var msecs = (options.msecs != null) ? options.msecs : new Date().getTime();
149 |
150 | // Per 4.2.1.2, use count of uuid's generated during the current clock
151 | // cycle to simulate higher resolution clock
152 | var nsecs = (options.nsecs != null) ? options.nsecs : _lastNSecs + 1;
153 |
154 | // Time since last uuid creation (in msecs)
155 | var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000;
156 |
157 | // Per 4.2.1.2, Bump clockseq on clock regression
158 | if (dt < 0 && options.clockseq == null) {
159 | clockseq = clockseq + 1 & 0x3fff;
160 | }
161 |
162 | // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
163 | // time interval
164 | if ((dt < 0 || msecs > _lastMSecs) && options.nsecs == null) {
165 | nsecs = 0;
166 | }
167 |
168 | // Per 4.2.1.2 Throw error if too many uuids are requested
169 | if (nsecs >= 10000) {
170 | throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec');
171 | }
172 |
173 | _lastMSecs = msecs;
174 | _lastNSecs = nsecs;
175 | _clockseq = clockseq;
176 |
177 | // Per 4.1.4 - Convert from unix epoch to Gregorian epoch
178 | msecs += 12219292800000;
179 |
180 | // `time_low`
181 | var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
182 | b[i++] = tl >>> 24 & 0xff;
183 | b[i++] = tl >>> 16 & 0xff;
184 | b[i++] = tl >>> 8 & 0xff;
185 | b[i++] = tl & 0xff;
186 |
187 | // `time_mid`
188 | var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff;
189 | b[i++] = tmh >>> 8 & 0xff;
190 | b[i++] = tmh & 0xff;
191 |
192 | // `time_high_and_version`
193 | b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
194 | b[i++] = tmh >>> 16 & 0xff;
195 |
196 | // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
197 | b[i++] = clockseq >>> 8 | 0x80;
198 |
199 | // `clock_seq_low`
200 | b[i++] = clockseq & 0xff;
201 |
202 | // `node`
203 | var node = options.node || _nodeId;
204 | for (var n = 0; n < 6; n++) {
205 | b[i + n] = node[n];
206 | }
207 |
208 | return buf ? buf : unparse(b);
209 | }
210 |
211 | // **`v4()` - Generate random UUID**
212 |
213 | // See https://github.com/broofa/node-uuid for API details
214 | function v4(options, buf, offset) {
215 | // Deprecated - 'format' argument, as supported in v1.2
216 | var i = buf && offset || 0;
217 |
218 | if (typeof(options) === 'string') {
219 | buf = (options === 'binary') ? new BufferClass(16) : null;
220 | options = null;
221 | }
222 | options = options || {};
223 |
224 | var rnds = options.random || (options.rng || _rng)();
225 |
226 | // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
227 | rnds[6] = (rnds[6] & 0x0f) | 0x40;
228 | rnds[8] = (rnds[8] & 0x3f) | 0x80;
229 |
230 | // Copy bytes to buffer, if provided
231 | if (buf) {
232 | for (var ii = 0; ii < 16; ii++) {
233 | buf[i + ii] = rnds[ii];
234 | }
235 | }
236 |
237 | return buf || unparse(rnds);
238 | }
239 |
240 | // Export public API
241 | var uuid = v4;
242 | uuid.v1 = v1;
243 | uuid.v4 = v4;
244 | uuid.parse = parse;
245 | uuid.unparse = unparse;
246 | uuid.BufferClass = BufferClass;
247 | uuid._rng = _rng;
248 | uuid._mathRNG = _mathRNG;
249 | uuid._nodeRNG = _nodeRNG;
250 | uuid._whatwgRNG = _whatwgRNG;
251 |
252 | if (('undefined' !== typeof module) && module.exports) {
253 | // Publish as node.js module
254 | module.exports = uuid;
255 | } else if (typeof define === 'function' && define.amd) {
256 | // Publish as AMD module
257 | define(function() {return uuid;});
258 |
259 |
260 | } else {
261 | // Publish as global (in browsers)
262 | _previousRoot = _window.uuid;
263 |
264 | // **`noConflict()` - (browser only) to reset global 'uuid' var**
265 | uuid.noConflict = function() {
266 | _window.uuid = _previousRoot;
267 | return uuid;
268 | };
269 |
270 | _window.uuid = uuid;
271 | }
272 | })('undefined' !== typeof window ? window : null);
273 |
--------------------------------------------------------------------------------
/examples/esm-pixi/lib/uuid.js:
--------------------------------------------------------------------------------
1 | // uuid.js
2 | //
3 | // Copyright (c) 2010-2012 Robert Kieffer
4 | // MIT License - http://opensource.org/licenses/mit-license.php
5 |
6 | /*global window, require, define */
7 | (function(_window) {
8 | 'use strict';
9 |
10 | // Unique ID creation requires a high quality random # generator. We feature
11 | // detect to determine the best RNG source, normalizing to a function that
12 | // returns 128-bits of randomness, since that's what's usually required
13 | var _rng, _mathRNG, _nodeRNG, _whatwgRNG, _previousRoot;
14 |
15 | function setupBrowser() {
16 | // Allow for MSIE11 msCrypto
17 | var _crypto = _window.crypto || _window.msCrypto;
18 |
19 | if (!_rng && _crypto && _crypto.getRandomValues) {
20 | // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto
21 | //
22 | // Moderately fast, high quality
23 | try {
24 | var _rnds8 = new Uint8Array(16);
25 | _whatwgRNG = _rng = function whatwgRNG() {
26 | _crypto.getRandomValues(_rnds8);
27 | return _rnds8;
28 | };
29 | _rng();
30 | } catch(e) {}
31 | }
32 |
33 | if (!_rng) {
34 | // Math.random()-based (RNG)
35 | //
36 | // If all else fails, use Math.random(). It's fast, but is of unspecified
37 | // quality.
38 | var _rnds = new Array(16);
39 | _mathRNG = _rng = function() {
40 | for (var i = 0, r; i < 16; i++) {
41 | if ((i & 0x03) === 0) { r = Math.random() * 0x100000000; }
42 | _rnds[i] = r >>> ((i & 0x03) << 3) & 0xff;
43 | }
44 |
45 | return _rnds;
46 | };
47 | if ('undefined' !== typeof console && console.warn) {
48 | console.warn("[SECURITY] node-uuid: crypto not usable, falling back to insecure Math.random()");
49 | }
50 | }
51 | }
52 |
53 | function setupNode() {
54 | // Node.js crypto-based RNG - http://nodejs.org/docs/v0.6.2/api/crypto.html
55 | //
56 | // Moderately fast, high quality
57 | if ('function' === typeof require) {
58 | try {
59 | var _rb = require('crypto').randomBytes;
60 | _nodeRNG = _rng = _rb && function() {return _rb(16);};
61 | _rng();
62 | } catch(e) {}
63 | }
64 | }
65 |
66 | if (_window) {
67 | setupBrowser();
68 | } else {
69 | setupNode();
70 | }
71 |
72 | // Buffer class to use
73 | var BufferClass = ('function' === typeof Buffer) ? Buffer : Array;
74 |
75 | // Maps for number <-> hex string conversion
76 | var _byteToHex = [];
77 | var _hexToByte = {};
78 | for (var i = 0; i < 256; i++) {
79 | _byteToHex[i] = (i + 0x100).toString(16).substr(1);
80 | _hexToByte[_byteToHex[i]] = i;
81 | }
82 |
83 | // **`parse()` - Parse a UUID into it's component bytes**
84 | function parse(s, buf, offset) {
85 | var i = (buf && offset) || 0, ii = 0;
86 |
87 | buf = buf || [];
88 | s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) {
89 | if (ii < 16) { // Don't overflow!
90 | buf[i + ii++] = _hexToByte[oct];
91 | }
92 | });
93 |
94 | // Zero out remaining bytes if string was short
95 | while (ii < 16) {
96 | buf[i + ii++] = 0;
97 | }
98 |
99 | return buf;
100 | }
101 |
102 | // **`unparse()` - Convert UUID byte array (ala parse()) into a string**
103 | function unparse(buf, offset) {
104 | var i = offset || 0, bth = _byteToHex;
105 | return bth[buf[i++]] + bth[buf[i++]] +
106 | bth[buf[i++]] + bth[buf[i++]] + '-' +
107 | bth[buf[i++]] + bth[buf[i++]] + '-' +
108 | bth[buf[i++]] + bth[buf[i++]] + '-' +
109 | bth[buf[i++]] + bth[buf[i++]] + '-' +
110 | bth[buf[i++]] + bth[buf[i++]] +
111 | bth[buf[i++]] + bth[buf[i++]] +
112 | bth[buf[i++]] + bth[buf[i++]];
113 | }
114 |
115 | // **`v1()` - Generate time-based UUID**
116 | //
117 | // Inspired by https://github.com/LiosK/UUID.js
118 | // and http://docs.python.org/library/uuid.html
119 |
120 | // random #'s we need to init node and clockseq
121 | var _seedBytes = _rng();
122 |
123 | // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
124 | var _nodeId = [
125 | _seedBytes[0] | 0x01,
126 | _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]
127 | ];
128 |
129 | // Per 4.2.2, randomize (14 bit) clockseq
130 | var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff;
131 |
132 | // Previous uuid creation time
133 | var _lastMSecs = 0, _lastNSecs = 0;
134 |
135 | // See https://github.com/broofa/node-uuid for API details
136 | function v1(options, buf, offset) {
137 | var i = buf && offset || 0;
138 | var b = buf || [];
139 |
140 | options = options || {};
141 |
142 | var clockseq = (options.clockseq != null) ? options.clockseq : _clockseq;
143 |
144 | // UUID timestamps are 100 nano-second units since the Gregorian epoch,
145 | // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so
146 | // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
147 | // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
148 | var msecs = (options.msecs != null) ? options.msecs : new Date().getTime();
149 |
150 | // Per 4.2.1.2, use count of uuid's generated during the current clock
151 | // cycle to simulate higher resolution clock
152 | var nsecs = (options.nsecs != null) ? options.nsecs : _lastNSecs + 1;
153 |
154 | // Time since last uuid creation (in msecs)
155 | var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000;
156 |
157 | // Per 4.2.1.2, Bump clockseq on clock regression
158 | if (dt < 0 && options.clockseq == null) {
159 | clockseq = clockseq + 1 & 0x3fff;
160 | }
161 |
162 | // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
163 | // time interval
164 | if ((dt < 0 || msecs > _lastMSecs) && options.nsecs == null) {
165 | nsecs = 0;
166 | }
167 |
168 | // Per 4.2.1.2 Throw error if too many uuids are requested
169 | if (nsecs >= 10000) {
170 | throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec');
171 | }
172 |
173 | _lastMSecs = msecs;
174 | _lastNSecs = nsecs;
175 | _clockseq = clockseq;
176 |
177 | // Per 4.1.4 - Convert from unix epoch to Gregorian epoch
178 | msecs += 12219292800000;
179 |
180 | // `time_low`
181 | var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
182 | b[i++] = tl >>> 24 & 0xff;
183 | b[i++] = tl >>> 16 & 0xff;
184 | b[i++] = tl >>> 8 & 0xff;
185 | b[i++] = tl & 0xff;
186 |
187 | // `time_mid`
188 | var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff;
189 | b[i++] = tmh >>> 8 & 0xff;
190 | b[i++] = tmh & 0xff;
191 |
192 | // `time_high_and_version`
193 | b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
194 | b[i++] = tmh >>> 16 & 0xff;
195 |
196 | // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
197 | b[i++] = clockseq >>> 8 | 0x80;
198 |
199 | // `clock_seq_low`
200 | b[i++] = clockseq & 0xff;
201 |
202 | // `node`
203 | var node = options.node || _nodeId;
204 | for (var n = 0; n < 6; n++) {
205 | b[i + n] = node[n];
206 | }
207 |
208 | return buf ? buf : unparse(b);
209 | }
210 |
211 | // **`v4()` - Generate random UUID**
212 |
213 | // See https://github.com/broofa/node-uuid for API details
214 | function v4(options, buf, offset) {
215 | // Deprecated - 'format' argument, as supported in v1.2
216 | var i = buf && offset || 0;
217 |
218 | if (typeof(options) === 'string') {
219 | buf = (options === 'binary') ? new BufferClass(16) : null;
220 | options = null;
221 | }
222 | options = options || {};
223 |
224 | var rnds = options.random || (options.rng || _rng)();
225 |
226 | // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
227 | rnds[6] = (rnds[6] & 0x0f) | 0x40;
228 | rnds[8] = (rnds[8] & 0x3f) | 0x80;
229 |
230 | // Copy bytes to buffer, if provided
231 | if (buf) {
232 | for (var ii = 0; ii < 16; ii++) {
233 | buf[i + ii] = rnds[ii];
234 | }
235 | }
236 |
237 | return buf || unparse(rnds);
238 | }
239 |
240 | // Export public API
241 | var uuid = v4;
242 | uuid.v1 = v1;
243 | uuid.v4 = v4;
244 | uuid.parse = parse;
245 | uuid.unparse = unparse;
246 | uuid.BufferClass = BufferClass;
247 | uuid._rng = _rng;
248 | uuid._mathRNG = _mathRNG;
249 | uuid._nodeRNG = _nodeRNG;
250 | uuid._whatwgRNG = _whatwgRNG;
251 |
252 | if (('undefined' !== typeof module) && module.exports) {
253 | // Publish as node.js module
254 | module.exports = uuid;
255 | } else if (typeof define === 'function' && define.amd) {
256 | // Publish as AMD module
257 | define(function() {return uuid;});
258 |
259 |
260 | } else {
261 | // Publish as global (in browsers)
262 | _previousRoot = _window.uuid;
263 |
264 | // **`noConflict()` - (browser only) to reset global 'uuid' var**
265 | uuid.noConflict = function() {
266 | _window.uuid = _previousRoot;
267 | return uuid;
268 | };
269 |
270 | _window.uuid = uuid;
271 | }
272 | })('undefined' !== typeof window ? window : null);
273 |
--------------------------------------------------------------------------------
/examples/webpack-coffee/lib/uuid.js:
--------------------------------------------------------------------------------
1 | // uuid.js
2 | //
3 | // Copyright (c) 2010-2012 Robert Kieffer
4 | // MIT License - http://opensource.org/licenses/mit-license.php
5 |
6 | /*global window, require, define */
7 | (function(_window) {
8 | 'use strict';
9 |
10 | // Unique ID creation requires a high quality random # generator. We feature
11 | // detect to determine the best RNG source, normalizing to a function that
12 | // returns 128-bits of randomness, since that's what's usually required
13 | var _rng, _mathRNG, _nodeRNG, _whatwgRNG, _previousRoot;
14 |
15 | function setupBrowser() {
16 | // Allow for MSIE11 msCrypto
17 | var _crypto = _window.crypto || _window.msCrypto;
18 |
19 | if (!_rng && _crypto && _crypto.getRandomValues) {
20 | // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto
21 | //
22 | // Moderately fast, high quality
23 | try {
24 | var _rnds8 = new Uint8Array(16);
25 | _whatwgRNG = _rng = function whatwgRNG() {
26 | _crypto.getRandomValues(_rnds8);
27 | return _rnds8;
28 | };
29 | _rng();
30 | } catch(e) {}
31 | }
32 |
33 | if (!_rng) {
34 | // Math.random()-based (RNG)
35 | //
36 | // If all else fails, use Math.random(). It's fast, but is of unspecified
37 | // quality.
38 | var _rnds = new Array(16);
39 | _mathRNG = _rng = function() {
40 | for (var i = 0, r; i < 16; i++) {
41 | if ((i & 0x03) === 0) { r = Math.random() * 0x100000000; }
42 | _rnds[i] = r >>> ((i & 0x03) << 3) & 0xff;
43 | }
44 |
45 | return _rnds;
46 | };
47 | if ('undefined' !== typeof console && console.warn) {
48 | console.warn("[SECURITY] node-uuid: crypto not usable, falling back to insecure Math.random()");
49 | }
50 | }
51 | }
52 |
53 | function setupNode() {
54 | // Node.js crypto-based RNG - http://nodejs.org/docs/v0.6.2/api/crypto.html
55 | //
56 | // Moderately fast, high quality
57 | if ('function' === typeof require) {
58 | try {
59 | var _rb = require('crypto').randomBytes;
60 | _nodeRNG = _rng = _rb && function() {return _rb(16);};
61 | _rng();
62 | } catch(e) {}
63 | }
64 | }
65 |
66 | if (_window) {
67 | setupBrowser();
68 | } else {
69 | setupNode();
70 | }
71 |
72 | // Buffer class to use
73 | var BufferClass = ('function' === typeof Buffer) ? Buffer : Array;
74 |
75 | // Maps for number <-> hex string conversion
76 | var _byteToHex = [];
77 | var _hexToByte = {};
78 | for (var i = 0; i < 256; i++) {
79 | _byteToHex[i] = (i + 0x100).toString(16).substr(1);
80 | _hexToByte[_byteToHex[i]] = i;
81 | }
82 |
83 | // **`parse()` - Parse a UUID into it's component bytes**
84 | function parse(s, buf, offset) {
85 | var i = (buf && offset) || 0, ii = 0;
86 |
87 | buf = buf || [];
88 | s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) {
89 | if (ii < 16) { // Don't overflow!
90 | buf[i + ii++] = _hexToByte[oct];
91 | }
92 | });
93 |
94 | // Zero out remaining bytes if string was short
95 | while (ii < 16) {
96 | buf[i + ii++] = 0;
97 | }
98 |
99 | return buf;
100 | }
101 |
102 | // **`unparse()` - Convert UUID byte array (ala parse()) into a string**
103 | function unparse(buf, offset) {
104 | var i = offset || 0, bth = _byteToHex;
105 | return bth[buf[i++]] + bth[buf[i++]] +
106 | bth[buf[i++]] + bth[buf[i++]] + '-' +
107 | bth[buf[i++]] + bth[buf[i++]] + '-' +
108 | bth[buf[i++]] + bth[buf[i++]] + '-' +
109 | bth[buf[i++]] + bth[buf[i++]] + '-' +
110 | bth[buf[i++]] + bth[buf[i++]] +
111 | bth[buf[i++]] + bth[buf[i++]] +
112 | bth[buf[i++]] + bth[buf[i++]];
113 | }
114 |
115 | // **`v1()` - Generate time-based UUID**
116 | //
117 | // Inspired by https://github.com/LiosK/UUID.js
118 | // and http://docs.python.org/library/uuid.html
119 |
120 | // random #'s we need to init node and clockseq
121 | var _seedBytes = _rng();
122 |
123 | // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
124 | var _nodeId = [
125 | _seedBytes[0] | 0x01,
126 | _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]
127 | ];
128 |
129 | // Per 4.2.2, randomize (14 bit) clockseq
130 | var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff;
131 |
132 | // Previous uuid creation time
133 | var _lastMSecs = 0, _lastNSecs = 0;
134 |
135 | // See https://github.com/broofa/node-uuid for API details
136 | function v1(options, buf, offset) {
137 | var i = buf && offset || 0;
138 | var b = buf || [];
139 |
140 | options = options || {};
141 |
142 | var clockseq = (options.clockseq != null) ? options.clockseq : _clockseq;
143 |
144 | // UUID timestamps are 100 nano-second units since the Gregorian epoch,
145 | // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so
146 | // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
147 | // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
148 | var msecs = (options.msecs != null) ? options.msecs : new Date().getTime();
149 |
150 | // Per 4.2.1.2, use count of uuid's generated during the current clock
151 | // cycle to simulate higher resolution clock
152 | var nsecs = (options.nsecs != null) ? options.nsecs : _lastNSecs + 1;
153 |
154 | // Time since last uuid creation (in msecs)
155 | var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000;
156 |
157 | // Per 4.2.1.2, Bump clockseq on clock regression
158 | if (dt < 0 && options.clockseq == null) {
159 | clockseq = clockseq + 1 & 0x3fff;
160 | }
161 |
162 | // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
163 | // time interval
164 | if ((dt < 0 || msecs > _lastMSecs) && options.nsecs == null) {
165 | nsecs = 0;
166 | }
167 |
168 | // Per 4.2.1.2 Throw error if too many uuids are requested
169 | if (nsecs >= 10000) {
170 | throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec');
171 | }
172 |
173 | _lastMSecs = msecs;
174 | _lastNSecs = nsecs;
175 | _clockseq = clockseq;
176 |
177 | // Per 4.1.4 - Convert from unix epoch to Gregorian epoch
178 | msecs += 12219292800000;
179 |
180 | // `time_low`
181 | var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
182 | b[i++] = tl >>> 24 & 0xff;
183 | b[i++] = tl >>> 16 & 0xff;
184 | b[i++] = tl >>> 8 & 0xff;
185 | b[i++] = tl & 0xff;
186 |
187 | // `time_mid`
188 | var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff;
189 | b[i++] = tmh >>> 8 & 0xff;
190 | b[i++] = tmh & 0xff;
191 |
192 | // `time_high_and_version`
193 | b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
194 | b[i++] = tmh >>> 16 & 0xff;
195 |
196 | // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
197 | b[i++] = clockseq >>> 8 | 0x80;
198 |
199 | // `clock_seq_low`
200 | b[i++] = clockseq & 0xff;
201 |
202 | // `node`
203 | var node = options.node || _nodeId;
204 | for (var n = 0; n < 6; n++) {
205 | b[i + n] = node[n];
206 | }
207 |
208 | return buf ? buf : unparse(b);
209 | }
210 |
211 | // **`v4()` - Generate random UUID**
212 |
213 | // See https://github.com/broofa/node-uuid for API details
214 | function v4(options, buf, offset) {
215 | // Deprecated - 'format' argument, as supported in v1.2
216 | var i = buf && offset || 0;
217 |
218 | if (typeof(options) === 'string') {
219 | buf = (options === 'binary') ? new BufferClass(16) : null;
220 | options = null;
221 | }
222 | options = options || {};
223 |
224 | var rnds = options.random || (options.rng || _rng)();
225 |
226 | // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
227 | rnds[6] = (rnds[6] & 0x0f) | 0x40;
228 | rnds[8] = (rnds[8] & 0x3f) | 0x80;
229 |
230 | // Copy bytes to buffer, if provided
231 | if (buf) {
232 | for (var ii = 0; ii < 16; ii++) {
233 | buf[i + ii] = rnds[ii];
234 | }
235 | }
236 |
237 | return buf || unparse(rnds);
238 | }
239 |
240 | // Export public API
241 | var uuid = v4;
242 | uuid.v1 = v1;
243 | uuid.v4 = v4;
244 | uuid.parse = parse;
245 | uuid.unparse = unparse;
246 | uuid.BufferClass = BufferClass;
247 | uuid._rng = _rng;
248 | uuid._mathRNG = _mathRNG;
249 | uuid._nodeRNG = _nodeRNG;
250 | uuid._whatwgRNG = _whatwgRNG;
251 |
252 | if (('undefined' !== typeof module) && module.exports) {
253 | // Publish as node.js module
254 | module.exports = uuid;
255 | } else if (typeof define === 'function' && define.amd) {
256 | // Publish as AMD module
257 | define(function() {return uuid;});
258 |
259 |
260 | } else {
261 | // Publish as global (in browsers)
262 | _previousRoot = _window.uuid;
263 |
264 | // **`noConflict()` - (browser only) to reset global 'uuid' var**
265 | uuid.noConflict = function() {
266 | _window.uuid = _previousRoot;
267 | return uuid;
268 | };
269 |
270 | _window.uuid = uuid;
271 | }
272 | })('undefined' !== typeof window ? window : null);
273 |
--------------------------------------------------------------------------------
/examples/script-tag-coffee/lib/uuid.js:
--------------------------------------------------------------------------------
1 | // uuid.js
2 | //
3 | // Copyright (c) 2010-2012 Robert Kieffer
4 | // MIT License - http://opensource.org/licenses/mit-license.php
5 |
6 | /*global window, require, define */
7 | (function(_window) {
8 | 'use strict';
9 |
10 | // Unique ID creation requires a high quality random # generator. We feature
11 | // detect to determine the best RNG source, normalizing to a function that
12 | // returns 128-bits of randomness, since that's what's usually required
13 | var _rng, _mathRNG, _nodeRNG, _whatwgRNG, _previousRoot;
14 |
15 | function setupBrowser() {
16 | // Allow for MSIE11 msCrypto
17 | var _crypto = _window.crypto || _window.msCrypto;
18 |
19 | if (!_rng && _crypto && _crypto.getRandomValues) {
20 | // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto
21 | //
22 | // Moderately fast, high quality
23 | try {
24 | var _rnds8 = new Uint8Array(16);
25 | _whatwgRNG = _rng = function whatwgRNG() {
26 | _crypto.getRandomValues(_rnds8);
27 | return _rnds8;
28 | };
29 | _rng();
30 | } catch(e) {}
31 | }
32 |
33 | if (!_rng) {
34 | // Math.random()-based (RNG)
35 | //
36 | // If all else fails, use Math.random(). It's fast, but is of unspecified
37 | // quality.
38 | var _rnds = new Array(16);
39 | _mathRNG = _rng = function() {
40 | for (var i = 0, r; i < 16; i++) {
41 | if ((i & 0x03) === 0) { r = Math.random() * 0x100000000; }
42 | _rnds[i] = r >>> ((i & 0x03) << 3) & 0xff;
43 | }
44 |
45 | return _rnds;
46 | };
47 | if ('undefined' !== typeof console && console.warn) {
48 | console.warn("[SECURITY] node-uuid: crypto not usable, falling back to insecure Math.random()");
49 | }
50 | }
51 | }
52 |
53 | function setupNode() {
54 | // Node.js crypto-based RNG - http://nodejs.org/docs/v0.6.2/api/crypto.html
55 | //
56 | // Moderately fast, high quality
57 | if ('function' === typeof require) {
58 | try {
59 | var _rb = require('crypto').randomBytes;
60 | _nodeRNG = _rng = _rb && function() {return _rb(16);};
61 | _rng();
62 | } catch(e) {}
63 | }
64 | }
65 |
66 | if (_window) {
67 | setupBrowser();
68 | } else {
69 | setupNode();
70 | }
71 |
72 | // Buffer class to use
73 | var BufferClass = ('function' === typeof Buffer) ? Buffer : Array;
74 |
75 | // Maps for number <-> hex string conversion
76 | var _byteToHex = [];
77 | var _hexToByte = {};
78 | for (var i = 0; i < 256; i++) {
79 | _byteToHex[i] = (i + 0x100).toString(16).substr(1);
80 | _hexToByte[_byteToHex[i]] = i;
81 | }
82 |
83 | // **`parse()` - Parse a UUID into it's component bytes**
84 | function parse(s, buf, offset) {
85 | var i = (buf && offset) || 0, ii = 0;
86 |
87 | buf = buf || [];
88 | s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) {
89 | if (ii < 16) { // Don't overflow!
90 | buf[i + ii++] = _hexToByte[oct];
91 | }
92 | });
93 |
94 | // Zero out remaining bytes if string was short
95 | while (ii < 16) {
96 | buf[i + ii++] = 0;
97 | }
98 |
99 | return buf;
100 | }
101 |
102 | // **`unparse()` - Convert UUID byte array (ala parse()) into a string**
103 | function unparse(buf, offset) {
104 | var i = offset || 0, bth = _byteToHex;
105 | return bth[buf[i++]] + bth[buf[i++]] +
106 | bth[buf[i++]] + bth[buf[i++]] + '-' +
107 | bth[buf[i++]] + bth[buf[i++]] + '-' +
108 | bth[buf[i++]] + bth[buf[i++]] + '-' +
109 | bth[buf[i++]] + bth[buf[i++]] + '-' +
110 | bth[buf[i++]] + bth[buf[i++]] +
111 | bth[buf[i++]] + bth[buf[i++]] +
112 | bth[buf[i++]] + bth[buf[i++]];
113 | }
114 |
115 | // **`v1()` - Generate time-based UUID**
116 | //
117 | // Inspired by https://github.com/LiosK/UUID.js
118 | // and http://docs.python.org/library/uuid.html
119 |
120 | // random #'s we need to init node and clockseq
121 | var _seedBytes = _rng();
122 |
123 | // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
124 | var _nodeId = [
125 | _seedBytes[0] | 0x01,
126 | _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]
127 | ];
128 |
129 | // Per 4.2.2, randomize (14 bit) clockseq
130 | var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff;
131 |
132 | // Previous uuid creation time
133 | var _lastMSecs = 0, _lastNSecs = 0;
134 |
135 | // See https://github.com/broofa/node-uuid for API details
136 | function v1(options, buf, offset) {
137 | var i = buf && offset || 0;
138 | var b = buf || [];
139 |
140 | options = options || {};
141 |
142 | var clockseq = (options.clockseq != null) ? options.clockseq : _clockseq;
143 |
144 | // UUID timestamps are 100 nano-second units since the Gregorian epoch,
145 | // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so
146 | // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
147 | // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
148 | var msecs = (options.msecs != null) ? options.msecs : new Date().getTime();
149 |
150 | // Per 4.2.1.2, use count of uuid's generated during the current clock
151 | // cycle to simulate higher resolution clock
152 | var nsecs = (options.nsecs != null) ? options.nsecs : _lastNSecs + 1;
153 |
154 | // Time since last uuid creation (in msecs)
155 | var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000;
156 |
157 | // Per 4.2.1.2, Bump clockseq on clock regression
158 | if (dt < 0 && options.clockseq == null) {
159 | clockseq = clockseq + 1 & 0x3fff;
160 | }
161 |
162 | // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
163 | // time interval
164 | if ((dt < 0 || msecs > _lastMSecs) && options.nsecs == null) {
165 | nsecs = 0;
166 | }
167 |
168 | // Per 4.2.1.2 Throw error if too many uuids are requested
169 | if (nsecs >= 10000) {
170 | throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec');
171 | }
172 |
173 | _lastMSecs = msecs;
174 | _lastNSecs = nsecs;
175 | _clockseq = clockseq;
176 |
177 | // Per 4.1.4 - Convert from unix epoch to Gregorian epoch
178 | msecs += 12219292800000;
179 |
180 | // `time_low`
181 | var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
182 | b[i++] = tl >>> 24 & 0xff;
183 | b[i++] = tl >>> 16 & 0xff;
184 | b[i++] = tl >>> 8 & 0xff;
185 | b[i++] = tl & 0xff;
186 |
187 | // `time_mid`
188 | var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff;
189 | b[i++] = tmh >>> 8 & 0xff;
190 | b[i++] = tmh & 0xff;
191 |
192 | // `time_high_and_version`
193 | b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
194 | b[i++] = tmh >>> 16 & 0xff;
195 |
196 | // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
197 | b[i++] = clockseq >>> 8 | 0x80;
198 |
199 | // `clock_seq_low`
200 | b[i++] = clockseq & 0xff;
201 |
202 | // `node`
203 | var node = options.node || _nodeId;
204 | for (var n = 0; n < 6; n++) {
205 | b[i + n] = node[n];
206 | }
207 |
208 | return buf ? buf : unparse(b);
209 | }
210 |
211 | // **`v4()` - Generate random UUID**
212 |
213 | // See https://github.com/broofa/node-uuid for API details
214 | function v4(options, buf, offset) {
215 | // Deprecated - 'format' argument, as supported in v1.2
216 | var i = buf && offset || 0;
217 |
218 | if (typeof(options) === 'string') {
219 | buf = (options === 'binary') ? new BufferClass(16) : null;
220 | options = null;
221 | }
222 | options = options || {};
223 |
224 | var rnds = options.random || (options.rng || _rng)();
225 |
226 | // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
227 | rnds[6] = (rnds[6] & 0x0f) | 0x40;
228 | rnds[8] = (rnds[8] & 0x3f) | 0x80;
229 |
230 | // Copy bytes to buffer, if provided
231 | if (buf) {
232 | for (var ii = 0; ii < 16; ii++) {
233 | buf[i + ii] = rnds[ii];
234 | }
235 | }
236 |
237 | return buf || unparse(rnds);
238 | }
239 |
240 | // Export public API
241 | var uuid = v4;
242 | uuid.v1 = v1;
243 | uuid.v4 = v4;
244 | uuid.parse = parse;
245 | uuid.unparse = unparse;
246 | uuid.BufferClass = BufferClass;
247 | uuid._rng = _rng;
248 | uuid._mathRNG = _mathRNG;
249 | uuid._nodeRNG = _nodeRNG;
250 | uuid._whatwgRNG = _whatwgRNG;
251 |
252 | if (('undefined' !== typeof module) && module.exports) {
253 | // Publish as node.js module
254 | module.exports = uuid;
255 | } else if (typeof define === 'function' && define.amd) {
256 | // Publish as AMD module
257 | define(function() {return uuid;});
258 |
259 |
260 | } else {
261 | // Publish as global (in browsers)
262 | _previousRoot = _window.uuid;
263 |
264 | // **`noConflict()` - (browser only) to reset global 'uuid' var**
265 | uuid.noConflict = function() {
266 | _window.uuid = _previousRoot;
267 | return uuid;
268 | };
269 |
270 | _window.uuid = uuid;
271 | }
272 | })('undefined' !== typeof window ? window : null);
273 |
--------------------------------------------------------------------------------
/source/arcs-overlap-tests.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | arcsOverlap tests
7 |
8 |
28 |
29 |
30 |
31 |
243 |
244 |
245 |
--------------------------------------------------------------------------------
/examples/esm-pixi/source/entities/Snake.js:
--------------------------------------------------------------------------------
1 | import { Entity, addEntityClass, helpers } from "skele2d";
2 | const { distanceToLineSegment, closestPointOnLineSegment } = helpers;
3 | import { Texture, groupD8, Point, SimpleRope, Container, Renderer, Ticker } from "pixi.js";
4 |
5 | const snake_texture = Texture.from('assets/snake.png');
6 | snake_texture.rotate = groupD8.MIRROR_HORIZONTAL;
7 |
8 | export default class Snake extends Entity {
9 | constructor() {
10 | super();
11 | // relying on key order, so points & segments must not be named with simple numbers,
12 | // since numeric keys are sorted before other keys
13 | this.structure.addPoint("head");
14 | let previous_part_name = "head";
15 | for (let i = 1; i < 20; i++) {
16 | const part_name = `part_${i}`;
17 | previous_part_name = this.structure.addSegment({
18 | from: previous_part_name,
19 | to: part_name,
20 | name: part_name,
21 | length: 50,
22 | width: 40
23 | });
24 | }
25 |
26 | const parts_list = Object.values(this.structure.points).filter(part => part.name.match(/head|part/));
27 | for (let part_index = 0; part_index < parts_list.length; part_index++) {
28 | const part = parts_list[part_index];
29 | part.radius = 50; //- part_index*0.1
30 | part.vx = 0;
31 | part.vy = 0;
32 | }
33 |
34 | this.structure.points.head.radius *= 1.2;
35 |
36 | this.bbox_padding = 150;
37 | }
38 |
39 | toJSON() {
40 | const def = {};
41 | for (let k in this) {
42 | const v = this[k];
43 | if (!k.startsWith("$_")) {
44 | def[k] = v;
45 | }
46 | }
47 | return def;
48 | }
49 |
50 | initLayout() {
51 | for (let segment_name in this.structure.segments) {
52 | const segment = this.structure.segments[segment_name];
53 | segment.b.x = segment.a.x + segment.length;
54 | }
55 | }
56 |
57 | step(world) {
58 | let part, part_index, segment, segment_name;
59 | const parts_list = Object.values(this.structure.points).filter(part => part.name.match(/head|part/));
60 |
61 | // stop at end of the world
62 | for (part of parts_list) {
63 | if ((part.y + this.y) > 400) {
64 | return;
65 | }
66 | }
67 |
68 | // reset/init
69 | for (part of parts_list) {
70 | part.fx = 0;
71 | part.fy = 0;
72 | }
73 |
74 | // move
75 | const collision = point => world.collision(this.toWorld(point), {
76 | types: entity => !["Snake"].includes(entity.constructor.name)
77 | });
78 | const t = performance.now() / 1000;
79 | for (part_index = 0; part_index < parts_list.length; part_index++) {
80 | // part.x += part.vx
81 | // part.y += part.vy
82 | part = parts_list[part_index];
83 | const hit = collision(part);
84 | if (hit) {
85 | part.vx = 0;
86 | part.vy = 0;
87 | // Project the part's position back to the surface of the ground.
88 | // This is done by finding the closest point on the polygon's edges.
89 | let closest_distance = Infinity;
90 | let closest_segment = null;
91 | const part_world = this.toWorld(part);
92 | const part_in_hit_space = hit.fromWorld(part_world);
93 | for (segment_name in hit.structure.segments) {
94 | segment = hit.structure.segments[segment_name];
95 | const dist = distanceToLineSegment(part_in_hit_space, segment.a, segment.b);
96 | if ((dist < closest_distance) && (Math.hypot(segment.a.x - segment.b.x, segment.a.y - segment.b.y) > 0.1)) {
97 | closest_distance = dist;
98 | closest_segment = segment;
99 | }
100 | }
101 | if (closest_segment) {
102 | const closest_point_in_hit_space = closestPointOnLineSegment(part_in_hit_space, closest_segment.a, closest_segment.b);
103 | const closest_point_world = hit.toWorld(closest_point_in_hit_space);
104 | const closest_point_local = this.fromWorld(closest_point_world);
105 | part.x = closest_point_local.x;
106 | part.y = closest_point_local.y;
107 | }
108 | } else {
109 | part.vy += 0.5;
110 | part.vx *= 0.99;
111 | part.vy *= 0.99;
112 | // @structure.stepLayout({gravity: 0.005, collision})
113 | // @structure.stepLayout() for [0..10]
114 | // @structure.stepLayout({collision}) for [0..4]
115 | part.x += part.vx;
116 | part.y += part.vy;
117 | }
118 |
119 | // angular constraint pivoting on this part
120 | const relative_angle = ((Math.sin((Math.sin(t) * Math.PI) / 4) - 0.5) * Math.PI) / parts_list.length / 2;
121 | part.relative_angle = relative_angle;
122 | const prev_part = parts_list[part_index - 1];
123 | const next_part = parts_list[part_index + 1];
124 | if (prev_part && next_part) {
125 | this.accumulate_angular_constraint_forces(prev_part, next_part, part, relative_angle);
126 | }
127 | }
128 |
129 | // apply forces
130 | for (part of parts_list) {
131 | part.vx += part.fx;
132 | part.vy += part.fy;
133 | part.x += part.fx;
134 | part.y += part.fy;
135 | }
136 |
137 | // constrain distances
138 | for (let i = 0; i < 4; i++) {
139 | var delta_length, delta_x, delta_y, diff;
140 | for (segment_name in this.structure.segments) {
141 | segment = this.structure.segments[segment_name];
142 | delta_x = segment.a.x - segment.b.x;
143 | delta_y = segment.a.y - segment.b.y;
144 | delta_length = Math.sqrt((delta_x * delta_x) + (delta_y * delta_y));
145 | diff = (delta_length - segment.length) / delta_length;
146 | if (isFinite(diff)) {
147 | segment.a.x -= delta_x * 0.5 * diff;
148 | segment.a.y -= delta_y * 0.5 * diff;
149 | segment.b.x += delta_x * 0.5 * diff;
150 | segment.b.y += delta_y * 0.5 * diff;
151 | segment.a.vx -= delta_x * 0.5 * diff;
152 | segment.a.vy -= delta_y * 0.5 * diff;
153 | segment.b.vx += delta_x * 0.5 * diff;
154 | segment.b.vy += delta_y * 0.5 * diff;
155 | } else {
156 | console.warn("diff is not finite, for Snake distance constraint");
157 | }
158 | }
159 | // self-collision
160 | for (part_index = 0; part_index < parts_list.length; part_index++) {
161 | part = parts_list[part_index];
162 | for (let other_part_index = 0; other_part_index < parts_list.length; other_part_index++) { //when part_index isnt other_part_index
163 | const other_part = parts_list[other_part_index];
164 | if (Math.abs(part_index - other_part_index) < 3) {
165 | continue;
166 | }
167 | delta_x = part.x - other_part.x;
168 | delta_y = part.y - other_part.y;
169 | delta_length = Math.sqrt((delta_x * delta_x) + (delta_y * delta_y));
170 | const target_min_length = part.radius + other_part.radius;
171 | if (delta_length < target_min_length) {
172 | diff = (delta_length - target_min_length) / delta_length;
173 | if (isFinite(diff)) {
174 | part.x -= delta_x * 0.5 * diff;
175 | part.y -= delta_y * 0.5 * diff;
176 | other_part.x += delta_x * 0.5 * diff;
177 | other_part.y += delta_y * 0.5 * diff;
178 | part.vx -= delta_x * 0.5 * diff;
179 | part.vy -= delta_y * 0.5 * diff;
180 | other_part.vx += delta_x * 0.5 * diff;
181 | other_part.vy += delta_y * 0.5 * diff;
182 | } else {
183 | console.warn("diff is not finite, for Snake self-collision constraint");
184 | }
185 | }
186 | }
187 | }
188 | }
189 |
190 | }
191 |
192 | accumulate_angular_constraint_forces(a, b, pivot, relative_angle) {
193 | const angle_a = Math.atan2(a.y - b.y, a.x - b.x);
194 | const angle_b = Math.atan2(pivot.y - b.y, pivot.x - b.x);
195 | const angle_diff = (angle_a - angle_b) - relative_angle;
196 |
197 | // angle_diff *= 0.9
198 | const distance = Math.hypot(a.x - b.x, a.y - b.y);
199 | // distance_a = Math.hypot(a.x - pivot.x, a.y - pivot.y)
200 | // distance_b = Math.hypot(b.x - pivot.x, b.y - pivot.y)
201 | // angle_diff /= Math.max(1, (distance / 5) ** 2.4)
202 |
203 | const old_a = { x: a.x, y: a.y };
204 | const old_b = { x: b.x, y: b.y };
205 |
206 | // Rotate around pivot.
207 | const rot_matrix = [[Math.cos(angle_diff), Math.sin(angle_diff)], [-Math.sin(angle_diff), Math.cos(angle_diff)]];
208 | const rot_matrix_inverse = [[Math.cos(-angle_diff), Math.sin(-angle_diff)], [-Math.sin(-angle_diff), Math.cos(-angle_diff)]];
209 | for (const point of [a, b]) {
210 | // Translate and rotate.
211 | [point.x, point.y] = [point.x, point.y].map((value, index) =>
212 | ((point === a ? rot_matrix : rot_matrix_inverse)[index][0] * (point.x - pivot.x)) +
213 | ((point === a ? rot_matrix : rot_matrix_inverse)[index][1] * (point.y - pivot.y))
214 | );
215 | // Translate back.
216 | point.x += pivot.x;
217 | point.y += pivot.y;
218 | }
219 |
220 | const f = 0.5;
221 | // using individual distances can cause spinning (overall angular momentum from nothing)
222 | // f_a = f / Math.max(1, Math.max(0, distance_a - 3) ** 1)
223 | // f_b = f / Math.max(1, Math.max(0, distance_b - 3) ** 1)
224 | // using the combined distance conserves overall angular momentum,
225 | // to say nothing of the physicality of the rest of this system
226 | // but it's a clear difference in zero gravity
227 | const f_a = f / Math.max(1, Math.pow(Math.max(0, distance - 6), 1));
228 | const f_b = f / Math.max(1, Math.pow(Math.max(0, distance - 6), 1));
229 |
230 | // Turn difference in position into velocity.
231 | a.fx += (a.x - old_a.x) * f_a;
232 | a.fy += (a.y - old_a.y) * f_a;
233 | b.fx += (b.x - old_b.x) * f_b;
234 | b.fy += (b.y - old_b.y) * f_b;
235 |
236 | // Opposite force on pivot.
237 | pivot.fx -= (a.x - old_a.x) * f_a;
238 | pivot.fy -= (a.y - old_a.y) * f_a;
239 | pivot.fx -= (b.x - old_b.x) * f_b;
240 | pivot.fy -= (b.y - old_b.y) * f_b;
241 |
242 | // Restore old position.
243 | a.x = old_a.x;
244 | a.y = old_a.y;
245 | b.x = old_b.x;
246 | b.y = old_b.y;
247 | }
248 |
249 | destroy() {
250 | if (this.$_container) {
251 | this.$_container.destroy();
252 | this.$_container = null;
253 | }
254 | if (this.$_ticker) {
255 | this.$_ticker.remove(this.$_tick);
256 | this.$_ticker = null;
257 | this.$_tick = null;
258 | }
259 | }
260 |
261 | pixiUpdate(stage, ticker) {
262 | if (this.$_container) {
263 | return;
264 | }
265 |
266 | const rope_points = [];
267 |
268 | for (let point_name in this.structure.points) {
269 | const point = this.structure.points[point_name];
270 | rope_points.push(new Point(point.x, point.y));
271 | }
272 |
273 | const strip = new SimpleRope(snake_texture, rope_points);
274 |
275 | this.$_container = new Container();
276 | this.$_container.x = this.x;
277 | this.$_container.y = this.y;
278 |
279 | stage.addChild(this.$_container);
280 |
281 | this.$_container.addChild(strip);
282 |
283 | this.$_ticker = ticker;
284 | this.$_ticker.add(this.$_tick = () => {
285 | this.$_container.x = this.x;
286 | this.$_container.y = this.y;
287 | const iterable = Object.values(this.structure.points);
288 | for (let i = 0; i < iterable.length; i++) {
289 | const part = iterable[i];
290 | rope_points[i].x = part.x;
291 | rope_points[i].y = part.y;
292 | }
293 | });
294 | }
295 |
296 | draw(ctx, view, world) {
297 | if (view.is_preview) {
298 | // Skele2D isn't set up to handle PIXI rendering for preview in the entities bar.
299 | // Create and draw PIXI canvas to preview canvas.
300 | if (!this.$_preview_pixi_renderer) {
301 | this.$_preview_pixi_renderer = new Renderer({
302 | width: view.width,
303 | height: view.height,
304 | backgroundAlpha: 0,
305 | antialias: true,
306 | resolution: 1,
307 | });
308 | }
309 | if (!this.$_preview_pixi_stage) {
310 | this.$_preview_pixi_stage = new Container();
311 | }
312 | this.$_preview_pixi_stage.x = (-view.center_x * view.scale) + (view.width / 2);
313 | this.$_preview_pixi_stage.y = (-view.center_y * view.scale) + (view.height / 2);
314 | this.$_preview_pixi_stage.scale.x = view.scale;
315 | this.$_preview_pixi_stage.scale.y = view.scale;
316 | this.$_preview_pixi_ticker = new Ticker();
317 | this.$_preview_pixi_ticker.autoStart = false;
318 | this.$_preview_pixi_ticker.stop();
319 | this.pixiUpdate(this.$_preview_pixi_stage, this.$_preview_pixi_ticker);
320 | this.$_preview_pixi_renderer.render(this.$_preview_pixi_stage);
321 | // Undo view transform since we're handling the transform with PIXI.
322 | ctx.setTransform(1, 0, 0, 1, 0, 0);
323 | ctx.drawImage(this.$_preview_pixi_renderer.view, 0, 0);
324 | }
325 | }
326 | };
327 | addEntityClass(Snake);
328 |
329 | // This is a temporary holdover until I make Skele2D call a destroy() method on entities.
330 | // I can also probably find some cleaner patterns for cleaning up PIXI stuff.
331 | // This is my first time using PIXI.
332 | // Skele2D sets `destroyed` to true when you delete an entity in the editor.
333 | // Hm, it doesn't when you undo/redo, though, so this is still leaving behind PIXI objects in that case.
334 | Object.defineProperty(Snake.prototype, "destroyed", {
335 | configurable: true,
336 | get() {
337 | return this.$_destroyed;
338 | },
339 | set(value) {
340 | this.$_destroyed = value;
341 | if (value) {
342 | this.destroy();
343 | }
344 | }
345 | });
346 |
--------------------------------------------------------------------------------