├── .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 | 16 | 31 | 34 | 35 | 37 | 41 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /source/icons/smooth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 31 | 34 | 35 | 37 | 41 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /source/icons/select.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 31 | 34 | 35 | 37 | 41 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /source/icons/roughen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 31 | 34 | 35 | 37 | 41 | 46 | 47 | 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 | 17 | 32 | 35 | 36 | 38 | 42 | 48 | 53 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /source/icons/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 31 | 34 | 35 | 37 | 41 | 46 | 51 | 52 | 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 | 16 | 31 | 34 | 35 | 37 | 41 | 47 | 51 | 52 | 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 | 16 | 31 | 34 | 35 | 37 | 41 | 46 | 51 | 52 | 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 | 16 | 31 | 34 | 35 | 37 | 41 | 46 | 51 | 56 | 61 | 62 | 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 | 16 | 31 | 34 | 35 | 37 | 41 | 46 | 51 | 56 | 61 | 62 | 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 | --------------------------------------------------------------------------------