chore: initial
This commit is contained in:
commit
22fe6261b8
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
|
15
package.json
Normal file
15
package.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "@avocado/monorepo",
|
||||
"version": "1.0.0",
|
||||
"author": "cha0s",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"chai": "^4.2.0",
|
||||
"mocha": "6.0.2",
|
||||
"mochapack": "1.1.0",
|
||||
"webpack": "4.29.6"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "mochapack --watch --webpack-config webpack.test.config.js 'packages/**/*.spec.js'"
|
||||
}
|
||||
}
|
115
packages/behavior/context/globals.js
Normal file
115
packages/behavior/context/globals.js
Normal file
|
@ -0,0 +1,115 @@
|
|||
import {TickingPromise} from '@avocado/core';
|
||||
|
||||
import {register as registerType} from './types/registry';
|
||||
import {Traversal} from '../item/traversal';
|
||||
|
||||
export class Globals {
|
||||
|
||||
log(any) {
|
||||
console.log(any);
|
||||
}
|
||||
|
||||
randomNumber(min, max, floor = true) {
|
||||
let mag = Math.random() * (max - min);
|
||||
return min + (floor ? Math.floor(mag) : mag);
|
||||
}
|
||||
|
||||
testing() {
|
||||
return null;
|
||||
}
|
||||
|
||||
waitMs(ms, state) {
|
||||
let waited = 0;
|
||||
let resolve;
|
||||
const promise = new TickingPromise(_resolve => resolve = _resolve);
|
||||
promise.ticker = (elapsed) => {
|
||||
if ((waited += elapsed) >= ms) {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Globals.
|
||||
registerType('globals', {
|
||||
label: 'Globals',
|
||||
children: {
|
||||
log: {
|
||||
type: {
|
||||
key: 'function',
|
||||
args: [
|
||||
{
|
||||
type: 'any',
|
||||
label: 'Thing to log',
|
||||
},
|
||||
],
|
||||
return: {
|
||||
type: 'void',
|
||||
},
|
||||
},
|
||||
label: 'Log to console',
|
||||
},
|
||||
randomNumber: {
|
||||
type: {
|
||||
key: 'function',
|
||||
args: [
|
||||
{
|
||||
type: {
|
||||
key: 'number',
|
||||
},
|
||||
label: 'Minimum value',
|
||||
advanced: true,
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
label: 'Maximum value',
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
label: 'Convert to an integer?',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
return: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
label: 'Random number',
|
||||
},
|
||||
test: {
|
||||
type: {
|
||||
key: 'function',
|
||||
args: [
|
||||
{
|
||||
type: 'number',
|
||||
label: 'Milliseconds to wait',
|
||||
},
|
||||
],
|
||||
return: {
|
||||
type: {
|
||||
key: 'entity',
|
||||
traits: ['mobile'],
|
||||
},
|
||||
},
|
||||
},
|
||||
label: 'Entityyyyy',
|
||||
},
|
||||
waitMs: {
|
||||
type: {
|
||||
key: 'function',
|
||||
args: [
|
||||
{
|
||||
type: 'number',
|
||||
label: 'Milliseconds to wait',
|
||||
},
|
||||
],
|
||||
return: {
|
||||
type: 'void',
|
||||
},
|
||||
},
|
||||
label: 'Wait for a specified time',
|
||||
},
|
||||
},
|
||||
});
|
99
packages/behavior/context/index.js
Normal file
99
packages/behavior/context/index.js
Normal file
|
@ -0,0 +1,99 @@
|
|||
import {Globals} from './globals';
|
||||
import {TypeMap} from './types';
|
||||
|
||||
class Context extends Map {
|
||||
|
||||
add(key, value) {
|
||||
this.set(key, value);
|
||||
}
|
||||
|
||||
renderStepsUntilNow(steps, step) {
|
||||
const stepsUntilNow = steps.slice(0, steps.indexOf(step));
|
||||
return TypeMap.renderSteps(stepsUntilNow);
|
||||
}
|
||||
|
||||
traverse(steps) {
|
||||
return this.traverseAndDo(steps, (node, step) => {
|
||||
return this.traverseOneStep(steps, node, step);
|
||||
});
|
||||
}
|
||||
|
||||
traverseAndDo(steps, fn) {
|
||||
const [first, ...rest] = steps;
|
||||
if ('key' !== first.type) {
|
||||
throw new TypeError(`First step in a traversal must be type "key"`);
|
||||
}
|
||||
return rest.reduce((walk, step, index) => {
|
||||
if (walk instanceof Promise) {
|
||||
return walk.then((walk) => {
|
||||
fn(walk, step, index);
|
||||
});
|
||||
}
|
||||
else {
|
||||
return fn(walk, step, index);
|
||||
}
|
||||
}, this.get(first.key));
|
||||
}
|
||||
|
||||
traverseAndSet(steps, value) {
|
||||
return this.traverseAndDo(steps, (node, step, index) => {
|
||||
const isLastStep = index === steps.length - 2;
|
||||
if (!isLastStep) {
|
||||
return this.traverseOneStep(steps, node, step);
|
||||
}
|
||||
switch (step.type) {
|
||||
case 'key':
|
||||
return node[step.key] = value.get(this);
|
||||
case 'invoke':
|
||||
const rendered = this.renderStepsUntilNow(steps, step);
|
||||
throw new ReferenceError(`invalid assignment to function "${rendered}"`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
traverseOneStep(steps, node, step) {
|
||||
if ('undefined' === typeof node) {
|
||||
const rendered = this.renderStepsUntilNow(steps, step);
|
||||
throw TypeError(`"${rendered}" is traversed through but undefined`);
|
||||
}
|
||||
switch (step.type) {
|
||||
case 'key':
|
||||
return node[step.key];
|
||||
case 'invoke':
|
||||
return node(...step.args.map((arg) => arg.get(this)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TypedContext extends Context {
|
||||
|
||||
constructor(iterator) {
|
||||
super(iterator);
|
||||
|
||||
this.types = {};
|
||||
}
|
||||
|
||||
add(key, value, type) {
|
||||
super.add(key, value);
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
this.types = {
|
||||
...this.types,
|
||||
[key]: type,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function createContext() {
|
||||
const context = new Context();
|
||||
context.add('global', new Globals());
|
||||
return context;
|
||||
}
|
||||
|
||||
export function createTypedContext() {
|
||||
const context = new TypedContext();
|
||||
context.add('global', new Globals(), 'globals');
|
||||
return context;
|
||||
}
|
143
packages/behavior/context/types/index.js
Normal file
143
packages/behavior/context/types/index.js
Normal file
|
@ -0,0 +1,143 @@
|
|||
import {get, register} from './registry';
|
||||
|
||||
export class TypeMap {
|
||||
|
||||
static normalizeSpec(type) {
|
||||
if ('string' === typeof type) {
|
||||
type = {key: type};
|
||||
}
|
||||
return {...get(type.key), type};
|
||||
}
|
||||
|
||||
static renderSteps(steps) {
|
||||
return steps.reduce((rendered, step) => {
|
||||
switch (step.type) {
|
||||
case 'key':
|
||||
if (rendered) {
|
||||
rendered += '.';
|
||||
}
|
||||
rendered += step.key;
|
||||
break;
|
||||
case 'invoke':
|
||||
rendered += `(${step.args.length > 0 ? '...' : ''})`;
|
||||
break;
|
||||
}
|
||||
return rendered;
|
||||
}, '');
|
||||
}
|
||||
|
||||
constructor(types) {
|
||||
this.types = types;
|
||||
}
|
||||
|
||||
get(matchType, options = {}) {
|
||||
options = {
|
||||
doIncludeAdvanced: false,
|
||||
onlyWritable: false,
|
||||
...options
|
||||
};
|
||||
const result = {};
|
||||
const typeMap = {};
|
||||
for (const key in this.types) {
|
||||
const spec = TypeMap.normalizeSpec(this.types[key])
|
||||
this.pushSpec(typeMap, options, spec, key, []);
|
||||
}
|
||||
for (const type in typeMap) {
|
||||
const spec = TypeMap.normalizeSpec(type);
|
||||
const doesMatch = (
|
||||
'any' === matchType
|
||||
|| type === matchType
|
||||
|| spec.matcher(matchType)
|
||||
);
|
||||
if (doesMatch) {
|
||||
result[type] = typeMap[type];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
pushSpec(typeMap, options, spec, key, steps) {
|
||||
steps = [...steps, {type: 'key', key}];
|
||||
this.pushTypeSteps(typeMap, options, spec, steps);
|
||||
if (spec.children) {
|
||||
this.pushSpecChildren(typeMap, options, spec.children, steps, spec);
|
||||
}
|
||||
spec.extraSteps(spec).forEach(({spec, steps: extraSteps}) => {
|
||||
extraSteps = [...steps, ...extraSteps];
|
||||
this.pushTypeSteps(typeMap, options, spec, extraSteps);
|
||||
if (spec.children) {
|
||||
this.pushSpecChildren(typeMap, options, spec.children, extraSteps, spec);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pushSpecChildren(typeMap, options, children, steps, spec) {
|
||||
children = 'function' === typeof children ? children(spec) : children;
|
||||
for (const childKey in children) {
|
||||
const child = children[childKey];
|
||||
const spec = TypeMap.normalizeSpec(child.type);
|
||||
const compoundChild = {...spec, ...child};
|
||||
if (compoundChild.advanced && !options.doIncludeAdvanced) {
|
||||
continue;
|
||||
}
|
||||
this.pushSpec(typeMap, options, compoundChild, childKey, steps);
|
||||
}
|
||||
}
|
||||
|
||||
pushTypeSteps(typeMap, options, spec, steps) {
|
||||
const {isWritable} = spec;
|
||||
if (options.onlyWritable && !isWritable) {
|
||||
return;
|
||||
}
|
||||
const {type: {key}} = spec;
|
||||
typeMap[key] = typeMap[key] || [];
|
||||
typeMap[key].push(steps);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Defaults...
|
||||
register('boolean', {
|
||||
label: 'A boolean value, either true or false',
|
||||
});
|
||||
register('function', {
|
||||
label: 'A function',
|
||||
children: {
|
||||
name: {
|
||||
advanced: true,
|
||||
type: 'string',
|
||||
label: 'Function name',
|
||||
},
|
||||
},
|
||||
valueMap: {
|
||||
invoke: (value, step) => {
|
||||
return value(...step.args);
|
||||
},
|
||||
},
|
||||
extraSteps: (spec) => {
|
||||
const returnSpec = TypeMap.normalizeSpec(spec.type.return.type);
|
||||
return [{
|
||||
spec: {
|
||||
...returnSpec,
|
||||
...spec.type.return,
|
||||
type: returnSpec.type,
|
||||
},
|
||||
steps: [{
|
||||
type: 'invoke',
|
||||
args: spec.type.args,
|
||||
}],
|
||||
}];
|
||||
}
|
||||
});
|
||||
register('number', {
|
||||
label: 'A numeric value',
|
||||
});
|
||||
register('object', {
|
||||
label: 'An object',
|
||||
});
|
||||
register('string', {
|
||||
label: 'A string of text',
|
||||
});
|
||||
register('void', {
|
||||
label: 'An undefined or unspecified value',
|
||||
});
|
26
packages/behavior/context/types/registry.js
Normal file
26
packages/behavior/context/types/registry.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
const types_PRIVATE = new Map();
|
||||
|
||||
export function get(type) {
|
||||
return types_PRIVATE.get(type);
|
||||
}
|
||||
|
||||
export function register(type, info) {
|
||||
info = {
|
||||
advanced: false,
|
||||
children: [],
|
||||
extraSteps: (spec) => {
|
||||
return [];
|
||||
},
|
||||
isWritable: false,
|
||||
matcher: (matchType) => {
|
||||
return false;
|
||||
},
|
||||
type,
|
||||
...info,
|
||||
};
|
||||
info.valueMap = {
|
||||
identity: (value, step) => { return value },
|
||||
...info.valueMap,
|
||||
}
|
||||
types_PRIVATE.set(type, info);
|
||||
}
|
11
packages/behavior/index.js
Normal file
11
packages/behavior/index.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
export {createContext, createTypedContext} from './context';
|
||||
export {TypeMap} from './context/types';
|
||||
export {
|
||||
get as getType,
|
||||
register as registerType
|
||||
} from './context/types/registry';
|
||||
|
||||
export {
|
||||
fromJSON as behaviorItemFromJSON,
|
||||
register as registerBehaviorItem
|
||||
} from './item/registry';
|
14
packages/behavior/item/action.js
Normal file
14
packages/behavior/item/action.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import {Traversal} from './traversal';
|
||||
|
||||
export class Action extends Traversal {
|
||||
static type() {
|
||||
return 'action';
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
type: 'action',
|
||||
};
|
||||
}
|
||||
}
|
68
packages/behavior/item/actions.js
Normal file
68
packages/behavior/item/actions.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
import {TickingPromise} from '@avocado/core';
|
||||
import {EventEmitter} from '@avocado/mixins';
|
||||
|
||||
import {Traversal} from './traversal';
|
||||
import {Traversals} from './traversals';
|
||||
|
||||
@EventEmitter
|
||||
export class Actions extends Traversals {
|
||||
|
||||
static type() {
|
||||
return 'actions';
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.index_PRIVATE = 0;
|
||||
this.traversals = [];
|
||||
this.pending = null;
|
||||
}
|
||||
|
||||
get index() {
|
||||
return this.index_PRIVATE;
|
||||
}
|
||||
|
||||
set index(index) {
|
||||
this.index_PRIVATE = index;
|
||||
}
|
||||
|
||||
tick(context, elapsed) {
|
||||
if (this.traversals.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.pending
|
||||
&& this.pending instanceof TickingPromise
|
||||
&& 'function' === typeof this.pending.ticker
|
||||
) {
|
||||
this.pending.ticker(elapsed);
|
||||
return;
|
||||
}
|
||||
|
||||
// Actions execute immediately until a promise is made, or they're all
|
||||
// executed.
|
||||
while (true) {
|
||||
const result = this.traversals[this.index].traverse(context);
|
||||
if (result instanceof Promise) {
|
||||
result.then(() => this.prologue());
|
||||
result.catch((error) => {
|
||||
throw error;
|
||||
});
|
||||
this.pending = result;
|
||||
break;
|
||||
}
|
||||
this.prologue();
|
||||
if (0 === this.index) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prologue() {
|
||||
this.pending = null;
|
||||
if (0 === (this.index = (this.index + 1) % this.invocations.length)) {
|
||||
this.emit('actionsFinished');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
0
packages/behavior/item/actions.spec.js
Normal file
0
packages/behavior/item/actions.spec.js
Normal file
35
packages/behavior/item/collection.js
Normal file
35
packages/behavior/item/collection.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
import {fromJSON as behaviorItemFromJSON} from './registry';
|
||||
|
||||
export function Collection(type) {
|
||||
|
||||
return class Collection {
|
||||
|
||||
static type() {
|
||||
return `${type}s`;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this[`${type}s`] = [];
|
||||
}
|
||||
|
||||
count() {
|
||||
return this[`${type}s`].length;
|
||||
}
|
||||
|
||||
fromJSON(json) {
|
||||
this[`${type}s`] = [];
|
||||
for (const i in json.items) {
|
||||
const item = json.items[i];
|
||||
this[`${type}s`].push(behaviorItemFromJSON(item.type, item));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
type,
|
||||
items: this[`${type}s`].map((item) => item.toJSON()),
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
97
packages/behavior/item/condition.js
Normal file
97
packages/behavior/item/condition.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
import {fromJSON as behaviorItemFromJSON} from './registry';
|
||||
|
||||
export class Condition {
|
||||
|
||||
static type() {
|
||||
return 'condition';
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.operator = '';
|
||||
this.operands = [];
|
||||
}
|
||||
|
||||
check(...args) {
|
||||
return this.get(...args);
|
||||
}
|
||||
|
||||
fromJSON(json) {
|
||||
this.operator = json.operator;
|
||||
this.operands = json.operands.map((operand) => {
|
||||
return behaviorItemFromJSON(operand);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
get(context) {
|
||||
switch (this.operator) {
|
||||
case 'is':
|
||||
return this.operands[0].get(context) === this.operands[1].get(context);
|
||||
case 'isnt':
|
||||
return this.operands[0].get(context) !== this.operands[1].get(context);
|
||||
case '>':
|
||||
return this.operands[0].get(context) > this.operands[1].get(context);
|
||||
case '>=':
|
||||
return this.operands[0].get(context) >= this.operands[1].get(context);
|
||||
case '<':
|
||||
return this.operands[0].get(context) < this.operands[1].get(context);
|
||||
case '<=':
|
||||
return this.operands[0].get(context) <= this.operands[1].get(context);
|
||||
|
||||
case 'or':
|
||||
if (0 === this.operands.length) {
|
||||
return true;
|
||||
}
|
||||
for (const operand of this.operands) {
|
||||
if (!!operand.get(context)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
case 'and':
|
||||
if (0 === this.operands.length) {
|
||||
return true;
|
||||
}
|
||||
for (const operand of this.operands) {
|
||||
if (!operand.get(context)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
operandCount() {
|
||||
return this.operands.length;
|
||||
}
|
||||
|
||||
operand(index) {
|
||||
return this.operands[index];
|
||||
}
|
||||
|
||||
operands() {
|
||||
return this.operands;
|
||||
}
|
||||
|
||||
operator() {
|
||||
return this.operator;
|
||||
}
|
||||
|
||||
setOperand(index, operand) {
|
||||
this.operands[index] = operand;
|
||||
}
|
||||
|
||||
setOperator(operator) {
|
||||
this.operator = operator;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
type: 'condition',
|
||||
operator: this.operator,
|
||||
operands: this.operands.map((operand) => operand.toJSON()),
|
||||
};
|
||||
}
|
||||
}
|
109
packages/behavior/item/condition.spec.js
Normal file
109
packages/behavior/item/condition.spec.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
import {expect} from 'chai';
|
||||
|
||||
import {Condition} from './condition';
|
||||
import {Literal} from './literal';
|
||||
import {deregister, register} from './registry';
|
||||
|
||||
describe('behavior', () => {
|
||||
describe('condition', () => {
|
||||
it('can do binary compares', () => {
|
||||
const condition = new Condition();
|
||||
condition.setOperand(0, new Literal().fromJSON({
|
||||
type: 'literal',
|
||||
value: 500,
|
||||
}));
|
||||
condition.setOperand(1, new Literal().fromJSON({
|
||||
type: 'literal',
|
||||
value: 420,
|
||||
}));
|
||||
// Greater than.
|
||||
condition.setOperator('>');
|
||||
expect(condition.check()).to.be.true;
|
||||
// Less than.
|
||||
condition.setOperator('<');
|
||||
expect(condition.check()).to.be.false;
|
||||
// Greater than or equal.
|
||||
condition.setOperator('>=');
|
||||
expect(condition.check()).to.be.true;
|
||||
// Less than or equal.
|
||||
condition.setOperator('<=');
|
||||
expect(condition.check()).to.be.false;
|
||||
// Is.
|
||||
condition.setOperator('is');
|
||||
expect(condition.check()).to.be.false;
|
||||
// Is not.
|
||||
condition.setOperator('isnt');
|
||||
expect(condition.check()).to.be.true;
|
||||
});
|
||||
it('can do varnary compares', () => {
|
||||
const condition = new Condition();
|
||||
condition.setOperand(0, new Literal().fromJSON({
|
||||
type: 'literal',
|
||||
value: true,
|
||||
}));
|
||||
condition.setOperand(1, new Literal().fromJSON({
|
||||
type: 'literal',
|
||||
value: true,
|
||||
}));
|
||||
condition.setOperand(2, new Literal().fromJSON({
|
||||
type: 'literal',
|
||||
value: false,
|
||||
}));
|
||||
// AND, mixed.
|
||||
condition.setOperator('and');
|
||||
expect(condition.check()).to.be.false;
|
||||
// OR, mixed.
|
||||
condition.setOperator('or');
|
||||
expect(condition.check()).to.be.true;
|
||||
// OR, all true.
|
||||
condition.setOperand(2, new Literal().fromJSON({
|
||||
type: 'literal',
|
||||
value: true,
|
||||
}));
|
||||
expect(condition.check()).to.be.true;
|
||||
// AND, all true.
|
||||
condition.setOperator('and');
|
||||
expect(condition.check()).to.be.true;
|
||||
// AND, all false.
|
||||
for (const i of [0, 1, 2]) {
|
||||
condition.setOperand(i, new Literal().fromJSON({
|
||||
type: 'literal',
|
||||
value: false,
|
||||
}));
|
||||
}
|
||||
expect(condition.check()).to.be.false;
|
||||
// OR, all false.
|
||||
condition.setOperator('or');
|
||||
expect(condition.check()).to.be.false;
|
||||
});
|
||||
describe('JSON', () => {
|
||||
beforeEach(() => {
|
||||
register(Literal);
|
||||
});
|
||||
afterEach(() => {
|
||||
deregister(Literal);
|
||||
});
|
||||
it('can instantiate operands', () => {
|
||||
const condition = new Condition();
|
||||
// Greater than.
|
||||
condition.fromJSON({
|
||||
operator: '>',
|
||||
operands: [
|
||||
{
|
||||
type: 'literal',
|
||||
value: 500,
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: 420,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(condition.check()).to.be.true;
|
||||
// Less than.
|
||||
condition.setOperator('<');
|
||||
expect(condition.check()).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
14
packages/behavior/item/conditions.js
Normal file
14
packages/behavior/item/conditions.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import {Collection} from './collection';
|
||||
|
||||
export class Conditions extends Collection('condition') {
|
||||
|
||||
check(context) {
|
||||
for (condition of this.conditions) {
|
||||
if (!this.conditions[index].check(context)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
23
packages/behavior/item/initialize.js
Normal file
23
packages/behavior/item/initialize.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import {register} from './registry';
|
||||
|
||||
import {Action} from './action';
|
||||
import {Actions} from './actions';
|
||||
import {Condition} from './condition';
|
||||
import {Conditions} from './conditions';
|
||||
import {Literal} from './literal';
|
||||
import {Routine} from './routine';
|
||||
import {Routines} from './routines';
|
||||
import {Traversal} from './traversal';
|
||||
import {Traversals} from './traversals';
|
||||
import {TraversalAndSet} from './traversal-and-set';
|
||||
|
||||
register(Action);
|
||||
register(Actions);
|
||||
register(Condition);
|
||||
register(Conditions);
|
||||
register(Literal);
|
||||
register(Routine);
|
||||
register(Routines);
|
||||
register(Traversal);
|
||||
register(Traversals);
|
||||
register(TraversalAndSet);
|
30
packages/behavior/item/literal.js
Normal file
30
packages/behavior/item/literal.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
export class Literal {
|
||||
|
||||
static type() {
|
||||
return 'literal';
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.value = null;
|
||||
}
|
||||
|
||||
fromJSON(json) {
|
||||
this.value = json.value;
|
||||
return this;
|
||||
}
|
||||
|
||||
get(context) {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
set(value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
type: 'literal',
|
||||
value: this.value,
|
||||
}
|
||||
}
|
||||
}
|
22
packages/behavior/item/literal.spec.js
Normal file
22
packages/behavior/item/literal.spec.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import {expect} from 'chai';
|
||||
|
||||
import {Literal} from './literal';
|
||||
|
||||
describe('behavior', () => {
|
||||
describe('literal', () => {
|
||||
let literal = undefined;
|
||||
beforeEach(() => {
|
||||
literal = new Literal();
|
||||
literal.fromJSON({
|
||||
value: 69,
|
||||
});
|
||||
});
|
||||
it('can access literals', () => {
|
||||
expect(literal.get()).to.equal(69);
|
||||
});
|
||||
it('can modify literals', () => {
|
||||
literal.set(420);
|
||||
expect(literal.get()).to.equal(420);
|
||||
});
|
||||
});
|
||||
});
|
17
packages/behavior/item/registry.js
Normal file
17
packages/behavior/item/registry.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
const behaviorItemRegistry = new Map();
|
||||
|
||||
export function fromJSON({type, ...json}) {
|
||||
const Class = behaviorItemRegistry.get(type);
|
||||
if (!Class) {
|
||||
throw new TypeError(`There is no class for the behavior item "${type}"`);
|
||||
}
|
||||
return (new Class()).fromJSON(json);
|
||||
}
|
||||
|
||||
export function deregister(BehaviorItem) {
|
||||
behaviorItemRegistry.delete(BehaviorItem.type());
|
||||
}
|
||||
|
||||
export function register(BehaviorItem) {
|
||||
behaviorItemRegistry.set(BehaviorItem.type(), BehaviorItem);
|
||||
}
|
29
packages/behavior/item/routine.js
Normal file
29
packages/behavior/item/routine.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import {Actions} from './actions';
|
||||
|
||||
export class Routine {
|
||||
|
||||
static type() {
|
||||
return 'routine';
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.actions = new Actions();
|
||||
}
|
||||
|
||||
fromJSON(json) {
|
||||
this.actions.fromObject(json.actions);
|
||||
return this;
|
||||
}
|
||||
|
||||
tick(context, elapsed) {
|
||||
this.actions.tick(context, elapsed);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
type: 'routine',
|
||||
actions: this.actions.toJSON(),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
34
packages/behavior/item/routines.js
Normal file
34
packages/behavior/item/routines.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import {Routine} from './routine';
|
||||
|
||||
export class Routines {
|
||||
|
||||
static type() {
|
||||
return 'routines';
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.routines = {};
|
||||
}
|
||||
|
||||
fromJSON(json) {
|
||||
for (const i in json.routines) {
|
||||
this.routines[i] = (new Routine()).fromJSON(json.routines[i]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
routine(index) {
|
||||
return this.routines[index];
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const routines = {};
|
||||
for (const i in this.routines) {
|
||||
routines[i] = this.routines[i].toJSON();
|
||||
}
|
||||
return {
|
||||
type: 'routines',
|
||||
routines,
|
||||
};
|
||||
}
|
||||
}
|
34
packages/behavior/item/traversal-and-set.js
Normal file
34
packages/behavior/item/traversal-and-set.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import {fromJSON as behaviorItemFromJSON} from './registry';
|
||||
import {Traversal} from './traversal';
|
||||
|
||||
export class TraversalAndSet extends Traversal {
|
||||
|
||||
static type() {
|
||||
return 'traversal-and-set';
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.value = undefined;
|
||||
}
|
||||
|
||||
fromJSON(json) {
|
||||
super.fromJSON(json);
|
||||
this.value = behaviorItemFromJSON(json.value);
|
||||
return this;
|
||||
}
|
||||
|
||||
traverse(context) {
|
||||
if (context) {
|
||||
return context.traverseAndSet(this.steps, this.value);
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
type: 'traversal-and-set',
|
||||
value: this.value.toJSON(),
|
||||
};
|
||||
}
|
||||
}
|
54
packages/behavior/item/traversal.js
Normal file
54
packages/behavior/item/traversal.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
import {fromJSON as behaviorItemFromJSON} from './registry';
|
||||
|
||||
export class Traversal {
|
||||
|
||||
static type() {
|
||||
return 'traversal';
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.steps = [];
|
||||
}
|
||||
|
||||
fromJSON(json) {
|
||||
this.steps = json.steps.map((step) => {
|
||||
switch (step.type) {
|
||||
case 'key':
|
||||
return step;
|
||||
case 'invoke':
|
||||
return {
|
||||
type: 'invoke',
|
||||
args: step.args.map((arg) => behaviorItemFromJSON(arg)),
|
||||
};
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
get(context) {
|
||||
return this.traverse(context);
|
||||
}
|
||||
|
||||
traverse(context) {
|
||||
if (context) {
|
||||
return context.traverse(this.steps);
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
type: 'traversal',
|
||||
steps: this.steps.map((step) => {
|
||||
switch (step.type) {
|
||||
case 'key':
|
||||
return step;
|
||||
case 'invoke':
|
||||
return {
|
||||
type: 'invoke',
|
||||
args: step.args.map((arg) => arg.toJSON()),
|
||||
};
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
36
packages/behavior/item/traversals.js
Normal file
36
packages/behavior/item/traversals.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import {TickingPromise} from '@avocado/core';
|
||||
|
||||
import {Collection} from './collection';
|
||||
import {Traversal} from './traversal';
|
||||
|
||||
export class Traversals extends Collection('traversal') {
|
||||
|
||||
parallel(context) {
|
||||
const results = this.traversals.map((traversal) => {
|
||||
return traversal.traverse(context);
|
||||
});
|
||||
// Early out if no promises.
|
||||
if (!results.reduce((has, result) => {
|
||||
return has || result instanceof Promise;
|
||||
}, false)) {
|
||||
return results;
|
||||
}
|
||||
// Otherwise, wrap in a promise.
|
||||
const tpromise = new TickingPromise((resolve, reject) => {
|
||||
return Promise.all(results);
|
||||
});
|
||||
// Proxy all ticks.
|
||||
const tickableResults = results.filter((result) => {
|
||||
if (!(result instanceof TickingPromise)) return false;
|
||||
if ('function' !== typeof result.ticker) return false;
|
||||
return true;
|
||||
});
|
||||
if (tickableResults.length > 0) {
|
||||
tpromise.ticker = (elapsed) => {
|
||||
tickableResults.forEach((result) => result.ticker(elapsed));
|
||||
};
|
||||
}
|
||||
return tpromise;
|
||||
}
|
||||
|
||||
}
|
10
packages/behavior/package.json
Normal file
10
packages/behavior/package.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "@avocado/behavior",
|
||||
"version": "1.0.4",
|
||||
"main": "index.js",
|
||||
"author": "cha0s",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "3.1.0"
|
||||
}
|
||||
}
|
1
packages/client/index.js
Normal file
1
packages/client/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './index.socket';
|
40
packages/client/index.socket.js
Normal file
40
packages/client/index.socket.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
import {EventEmitter} from 'events';
|
||||
import io from 'socket.io-client';
|
||||
|
||||
const exceptions = [
|
||||
'connect',
|
||||
];
|
||||
|
||||
class SocketClient extends EventEmitter {
|
||||
|
||||
constructor(address) {
|
||||
super();
|
||||
this.socket = io(address, {
|
||||
path: '/avocado',
|
||||
});
|
||||
this.socket.on('connect', () => {
|
||||
this.emit('connect');
|
||||
});
|
||||
this.socket.on('message', (...args) => {
|
||||
this.emit('message', ...args);
|
||||
});
|
||||
}
|
||||
|
||||
on(name, fn) {
|
||||
if (-1 === exceptions.indexOf(name)) {
|
||||
super.on(name, fn);
|
||||
}
|
||||
else {
|
||||
this.socket.on(name, fn);
|
||||
}
|
||||
}
|
||||
|
||||
send(...args) {
|
||||
this.socket.send(...args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function create(address) {
|
||||
return new SocketClient(address);
|
||||
}
|
10
packages/client/package.json
Normal file
10
packages/client/package.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "@avocado/client",
|
||||
"version": "1.0.1",
|
||||
"main": "index.js",
|
||||
"author": "cha0s",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"socket.io-client": "2.2.0"
|
||||
}
|
||||
}
|
45
packages/core/index.js
Normal file
45
packages/core/index.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Composes single-argument functions from right to left. The rightmost
|
||||
* function can take multiple arguments as it provides the signature for
|
||||
* the resulting composite function.
|
||||
*
|
||||
* @param {...Function} funcs The functions to compose.
|
||||
* @returns {Function} A function obtained by composing the argument functions
|
||||
* from right to left. For example, compose(f, g, h) is identical to doing
|
||||
* (...args) => f(g(h(...args))).
|
||||
*/
|
||||
|
||||
export function compose(...funcs) {
|
||||
if (funcs.length === 0) {
|
||||
return arg => arg
|
||||
}
|
||||
|
||||
if (funcs.length === 1) {
|
||||
return funcs[0]
|
||||
}
|
||||
|
||||
return funcs.reduce((a, b) => (...args) => a(b(...args)))
|
||||
}
|
||||
|
||||
export function virtualize(fields) {
|
||||
return (Superclass) => {
|
||||
class Virtualized extends Superclass {}
|
||||
fields.forEach((field) => {
|
||||
Virtualized.prototype[field] = function() {
|
||||
const prototype = Object.getPrototypeOf(this);
|
||||
const className = prototype.constructor.name;
|
||||
throw new ReferenceError(
|
||||
`"${className}" has undefined pure virtual method "${field}"`
|
||||
);
|
||||
}
|
||||
});
|
||||
return Virtualized;
|
||||
}
|
||||
}
|
||||
|
||||
export class TickingPromise extends Promise {
|
||||
constructor(resolve, reject) {
|
||||
super(resolve, reject);
|
||||
this.ticker = null;
|
||||
}
|
||||
}
|
7
packages/core/package.json
Normal file
7
packages/core/package.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@avocado/core",
|
||||
"version": "1.0.3",
|
||||
"main": "index.js",
|
||||
"author": "cha0s",
|
||||
"license": "MIT"
|
||||
}
|
143
packages/entity/index.js
Normal file
143
packages/entity/index.js
Normal file
|
@ -0,0 +1,143 @@
|
|||
import * as I from 'immutable';
|
||||
|
||||
import {compose} from '@avocado/core';
|
||||
import {EventEmitter} from '@avocado/mixins';
|
||||
import {Resource} from '@avocado/resource';
|
||||
|
||||
import {Traits} from './traits';
|
||||
|
||||
class TraitProxy {
|
||||
|
||||
has(entity, property, receiver) {
|
||||
if (property in entity) {
|
||||
return Reflect.has(entity, property, receiver);
|
||||
}
|
||||
else {
|
||||
return entity.traits_PRIVATE.hasProperty(property);
|
||||
}
|
||||
}
|
||||
|
||||
get(entity, property, receiver) {
|
||||
if (property in entity) {
|
||||
return Reflect.get(entity, property, receiver);
|
||||
}
|
||||
else {
|
||||
return entity.traits_PRIVATE.getProperty(property);
|
||||
}
|
||||
}
|
||||
|
||||
set(entity, property, value, receiver) {
|
||||
if (property in entity) {
|
||||
return Reflect.set(entity, property, value, receiver);
|
||||
}
|
||||
else {
|
||||
if (!entity.traits_PRIVATE.setProperty(property, value, receiver)) {
|
||||
return Reflect.set(entity, property, value, receiver);
|
||||
}
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const decorate = compose(
|
||||
EventEmitter,
|
||||
);
|
||||
|
||||
class Entity extends decorate(Resource) {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.isTicking_PRIVATE = true;
|
||||
this.traits_PRIVATE = new Traits(createProxy(this));
|
||||
}
|
||||
|
||||
acceptStateChange(change) {
|
||||
this.traits_PRIVATE.acceptStateChange(change);
|
||||
}
|
||||
|
||||
addTrait(type, trait) {
|
||||
this.traits_PRIVATE.addTrait(type, trait);
|
||||
}
|
||||
|
||||
addTraits(traits) {
|
||||
for (const type in traits) {
|
||||
this.traits_PRIVATE.addTrait(type, traits[type]);
|
||||
}
|
||||
}
|
||||
|
||||
allTraitInstances() {
|
||||
return this.traits_PRIVATE.allInstances();
|
||||
}
|
||||
|
||||
allTraitTypes() {
|
||||
return this.traits_PRIVATE.allTypes();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.isTicking = false;
|
||||
this.emit('destroyed');
|
||||
}
|
||||
|
||||
fromJSON(json) {
|
||||
super.fromJSON(json);
|
||||
this.traits_PRIVATE.fromJSON(json.traits);
|
||||
return this;
|
||||
}
|
||||
|
||||
hydrate() {
|
||||
return this.traits_PRIVATE.hydrate();
|
||||
}
|
||||
|
||||
invokeHook(hook, ...args) {
|
||||
return this.traits_PRIVATE.invokeHook(hook, ...args);
|
||||
}
|
||||
|
||||
invokeHookFlat(hook, ...args) {
|
||||
return this.traits_PRIVATE.invokeHookFlat(hook, ...args);
|
||||
}
|
||||
|
||||
removeAllTraits() {
|
||||
const types = this.traits_PRIVATE.allTypes();
|
||||
this.removeTraits(types);
|
||||
}
|
||||
|
||||
removeTrait(type) {
|
||||
this.traits_PRIVATE.removeTrait(type);
|
||||
}
|
||||
|
||||
removeTraits(types) {
|
||||
types.forEach((type) => this.removeTrait(type));
|
||||
}
|
||||
|
||||
state() {
|
||||
return this.traits_PRIVATE.state();
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
traits: this.traits_PRIVATE.toJSON(),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function create() {
|
||||
return createProxy(new Entity());
|
||||
}
|
||||
|
||||
export function createProxy(entity) {
|
||||
return new Proxy(entity, new TraitProxy());
|
||||
}
|
||||
|
||||
export {EntityList} from './list';
|
||||
|
||||
export {
|
||||
hasTrait,
|
||||
lookupTrait,
|
||||
registerTrait,
|
||||
} from './traits/registry';
|
||||
|
||||
export {simpleState, Trait} from './trait';
|
87
packages/entity/list.js
Normal file
87
packages/entity/list.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
import * as I from 'immutable';
|
||||
import mapValues from 'lodash.mapvalues';
|
||||
|
||||
import {create} from './index';
|
||||
|
||||
export class EntityList {
|
||||
|
||||
constructor() {
|
||||
this.entities_PRIVATE = {};
|
||||
this.state_PRIVATE = I.Map();
|
||||
this.uuidMap_PRIVATE = {};
|
||||
}
|
||||
|
||||
*[Symbol.iterator]() {
|
||||
for (const uuid in this.entities_PRIVATE) {
|
||||
const entity = this.entities_PRIVATE[uuid];
|
||||
yield entity;
|
||||
}
|
||||
}
|
||||
|
||||
acceptStateChange(change) {
|
||||
for (const uuid in change) {
|
||||
const localUuid = this.uuidMap_PRIVATE[uuid];
|
||||
const entity = this.entities_PRIVATE[localUuid];
|
||||
if (entity) {
|
||||
if (false === change[uuid]) {
|
||||
// Entity removed.
|
||||
this.removeEntity(entity);
|
||||
}
|
||||
else {
|
||||
entity.acceptStateChange(change[uuid]);
|
||||
this.state_PRIVATE = this.state_PRIVATE.set(localUuid, entity.state());
|
||||
}
|
||||
}
|
||||
else {
|
||||
// New entity. Create with change as traits' state.
|
||||
const traits = mapValues(change[uuid], (changeTraits) => ({
|
||||
state: changeTraits,
|
||||
}));
|
||||
const newEntity = create().fromJSON({traits});
|
||||
this.addEntity(newEntity);
|
||||
this.uuidMap_PRIVATE[uuid] = newEntity.instanceUuid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addEntity(entity) {
|
||||
const uuid = entity.instanceUuid;
|
||||
this.entities_PRIVATE[uuid] = entity;
|
||||
this.state_PRIVATE = this.state_PRIVATE.set(uuid, entity.state());
|
||||
entity.on('destroyed', () => {
|
||||
this.removeEntity(entity);
|
||||
});
|
||||
}
|
||||
|
||||
entity(uuid) {
|
||||
return this.entities_PRIVATE[uuid];
|
||||
}
|
||||
|
||||
recomputeState() {
|
||||
for (const uuid in this.entities_PRIVATE) {
|
||||
const entity = this.entities_PRIVATE[uuid];
|
||||
this.state_PRIVATE = this.state_PRIVATE.set(uuid, entity.state());
|
||||
}
|
||||
}
|
||||
|
||||
removeEntity(entity) {
|
||||
const uuid = entity.instanceUuid;
|
||||
delete this.entities_PRIVATE[uuid];
|
||||
this.state_PRIVATE = this.state_PRIVATE.delete(uuid);
|
||||
}
|
||||
|
||||
state() {
|
||||
return this.state_PRIVATE;
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const uuid in this.entities_PRIVATE) {
|
||||
const entity = this.entities_PRIVATE[uuid];
|
||||
if ('tick' in entity) {
|
||||
entity.tick(elapsed);
|
||||
}
|
||||
}
|
||||
this.recomputeState();
|
||||
}
|
||||
|
||||
}
|
17
packages/entity/package.json
Normal file
17
packages/entity/package.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "@avocado/entity",
|
||||
"version": "1.0.6",
|
||||
"main": "index.js",
|
||||
"author": "cha0s",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@avocado/core": "1.x",
|
||||
"@avocado/math": "1.x",
|
||||
"@avocado/mixins": "1.x",
|
||||
"@avocado/resource": "1.x",
|
||||
"debug": "^3.1.0",
|
||||
"immutable": "4.0.0-rc.12",
|
||||
"lodash.mapvalues": "4.6.0",
|
||||
"lodash.without": "4.4.0"
|
||||
}
|
||||
}
|
98
packages/entity/trait.js
Normal file
98
packages/entity/trait.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
import * as I from 'immutable';
|
||||
|
||||
import {Resource} from '@avocado/resource';
|
||||
|
||||
export class Trait {
|
||||
|
||||
constructor(entity) {
|
||||
this.entity = entity;
|
||||
this.params = I.fromJS(this.constructor.defaultParams());
|
||||
this.state = I.fromJS(this.constructor.defaultState());
|
||||
// Attach listeners.
|
||||
const listeners = this.listeners();
|
||||
const traitType = this.constructor.type();
|
||||
for (const type in this.listeners()) {
|
||||
entity.on(`${type}.trait-${traitType}`, listeners[type]);
|
||||
}
|
||||
}
|
||||
|
||||
acceptStateChange(change) {
|
||||
this.state = this.state.merge(change);
|
||||
}
|
||||
|
||||
actions() {
|
||||
return {};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.entity.off(`.trait-${this.constructor.type()}`);
|
||||
}
|
||||
|
||||
fromJSON({params = {}, state = {}}) {
|
||||
this.params = I.fromJS(this.constructor.defaultParams()).merge(params);
|
||||
this.state = I.fromJS(this.constructor.defaultState()).merge(state);
|
||||
return this;
|
||||
}
|
||||
|
||||
hooks() {
|
||||
return {};
|
||||
}
|
||||
|
||||
hydrate() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
label() {
|
||||
return this.constructor.name;
|
||||
}
|
||||
|
||||
listeners() {
|
||||
return {};
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
params: this.params.toJS(),
|
||||
state: this.state.toJS(),
|
||||
};
|
||||
}
|
||||
|
||||
static contextType() {
|
||||
return {};
|
||||
}
|
||||
|
||||
static defaultParams() {
|
||||
return {};
|
||||
}
|
||||
|
||||
static defaultState() {
|
||||
return {};
|
||||
}
|
||||
|
||||
static dependencies() {
|
||||
return [];
|
||||
}
|
||||
|
||||
static type() {
|
||||
return this.name.toLowerCase();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function simpleState(name) {
|
||||
return (Superclass) => {
|
||||
class SimpleState extends Superclass {}
|
||||
// Add simple state handler.
|
||||
Object.defineProperty(SimpleState.prototype, name, {
|
||||
get() {
|
||||
return this.state.get(name);
|
||||
},
|
||||
set(value) {
|
||||
this.state = this.state.set(name, value);
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
return SimpleState;
|
||||
}
|
||||
}
|
37
packages/entity/traits/directional.js
Normal file
37
packages/entity/traits/directional.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import {compose} from '@avocado/core';
|
||||
import {Vector} from '@avocado/math';
|
||||
|
||||
import {simpleState, Trait} from '../trait';
|
||||
|
||||
const decorate = compose(
|
||||
simpleState('direction'),
|
||||
);
|
||||
|
||||
class DirectionalBase extends Trait {
|
||||
|
||||
static defaultParams() {
|
||||
return {
|
||||
directionCount: 1,
|
||||
};
|
||||
}
|
||||
|
||||
static defaultState() {
|
||||
return {
|
||||
direction: 0,
|
||||
};
|
||||
}
|
||||
|
||||
listeners() {
|
||||
return {
|
||||
movementRequest: (vector) => {
|
||||
this.entity.direction = Vector.toDirection(
|
||||
vector,
|
||||
this.params.get('directionCount')
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class Directional extends decorate(DirectionalBase) {}
|
57
packages/entity/traits/existent.js
Normal file
57
packages/entity/traits/existent.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
import {compose} from '@avocado/core';
|
||||
|
||||
import {simpleState, Trait} from '../trait';
|
||||
|
||||
const decorate = compose(
|
||||
simpleState('name'),
|
||||
);
|
||||
|
||||
class ExistentBase extends Trait {
|
||||
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this._isTicking = this.params.get('isTicking');
|
||||
}
|
||||
|
||||
static defaultParams() {
|
||||
return {
|
||||
isTicking: true,
|
||||
};
|
||||
}
|
||||
|
||||
static defaultState() {
|
||||
return {
|
||||
name: 'Untitled entity',
|
||||
};
|
||||
}
|
||||
|
||||
get isTicking() {
|
||||
return this._isTicking;
|
||||
}
|
||||
|
||||
set isTicking(isTicking) {
|
||||
this._isTicking = isTicking;
|
||||
}
|
||||
|
||||
actions() {
|
||||
return {
|
||||
|
||||
destroy: () => {
|
||||
this.isTicking = false;
|
||||
this.entity.emit('destroy');
|
||||
this.entity.emit('destroyed');
|
||||
},
|
||||
|
||||
tick: (elapsed) => {
|
||||
if (!this.isTicking) {
|
||||
return;
|
||||
}
|
||||
this.entity.emit('tick', elapsed);
|
||||
},
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class Existent extends decorate(ExistentBase) {}
|
256
packages/entity/traits/index.js
Normal file
256
packages/entity/traits/index.js
Normal file
|
@ -0,0 +1,256 @@
|
|||
const debug = require('debug')('@avocado/entity:traits');
|
||||
import without from 'lodash.without';
|
||||
import * as I from 'immutable';
|
||||
|
||||
import {Resource} from '@avocado/resource';
|
||||
|
||||
import {hasTrait, lookupTrait, registerTrait} from './registry';
|
||||
|
||||
function enumerateProperties(prototype) {
|
||||
const result = {};
|
||||
do {
|
||||
Object.getOwnPropertyNames(prototype).forEach((property) => {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(prototype, property);
|
||||
if (typeof descriptor.get === 'function') {
|
||||
result[property] = result[property] || {};
|
||||
result[property].get = true;
|
||||
}
|
||||
if (typeof descriptor.set === 'function') {
|
||||
result[property] = result[property] || {};
|
||||
result[property].set = true;
|
||||
}
|
||||
});
|
||||
} while (Object.prototype !== (prototype = Object.getPrototypeOf(prototype)));
|
||||
return result;
|
||||
}
|
||||
|
||||
export class Traits {
|
||||
|
||||
constructor(entity) {
|
||||
this.actions_PRIVATE = {};
|
||||
this.entity_PRIVATE = entity;
|
||||
this.hooks_PRIVATE = {};
|
||||
this.properties_PRIVATE = {};
|
||||
this.state_PRIVATE = I.Map();
|
||||
this.traits_PRIVATE = {};
|
||||
}
|
||||
|
||||
acceptStateChange(change) {
|
||||
for (const type in change) {
|
||||
let instance = this.traits_PRIVATE[type];
|
||||
// New trait requested?
|
||||
if (!this.traits_PRIVATE[type]) {
|
||||
// Doesn't exist?
|
||||
if (!hasTrait(type)) {
|
||||
continue;
|
||||
}
|
||||
this.addTrait(type, change[type]);
|
||||
instance = this.traits_PRIVATE[type];
|
||||
}
|
||||
// Accept state.
|
||||
instance.acceptStateChange(change[type]);
|
||||
this.state_PRIVATE = this.state_PRIVATE.set(type, instance.state);
|
||||
}
|
||||
}
|
||||
|
||||
allInstances() {
|
||||
return this.traits_PRIVATE;
|
||||
}
|
||||
|
||||
allTypes() {
|
||||
return Object.keys(this.traits_PRIVATE);
|
||||
}
|
||||
|
||||
addTrait(type, json) {
|
||||
if (this.hasTrait(type)) {
|
||||
debug(`Tried to add trait "${type}" when it already exists!`);
|
||||
return;
|
||||
}
|
||||
if (!hasTrait(type)) {
|
||||
debug(`Tried to add trait "${type}" which isn't registered!`);
|
||||
return;
|
||||
}
|
||||
const Trait = lookupTrait(type);
|
||||
// Ensure dependencies.
|
||||
const dependencies = Trait.dependencies();
|
||||
const allTypes = this.allTypes();
|
||||
const lacking = without(dependencies, ...allTypes);
|
||||
if (lacking.length > 0) {
|
||||
debug(
|
||||
`Tried to add trait "${type}" but lack one or more dependents: "${
|
||||
lacking.join('", "')
|
||||
}"!`
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Instantiate.
|
||||
const instance = (new Trait(this.entity_PRIVATE)).fromJSON(json);
|
||||
// Proxy actions.
|
||||
const actions = instance.actions();
|
||||
for (const key in actions) {
|
||||
this.actions_PRIVATE[key] = actions[key];
|
||||
}
|
||||
// Register hook listeners.
|
||||
const hooks = instance.hooks();
|
||||
for (const key in hooks) {
|
||||
this.hooks_PRIVATE[key] = this.hooks_PRIVATE[key] || [];
|
||||
this.hooks_PRIVATE[key].push({
|
||||
fn: hooks[key],
|
||||
type: Trait.type(),
|
||||
});
|
||||
}
|
||||
// Proxy properties.
|
||||
const properties = enumerateProperties(Trait.prototype);
|
||||
for (const key in properties) {
|
||||
properties[key].instance = instance;
|
||||
this.properties_PRIVATE[key] = properties[key];
|
||||
}
|
||||
// Add state.
|
||||
this.state_PRIVATE = this.state_PRIVATE.set(type, instance.state);
|
||||
// Track trait.
|
||||
this.traits_PRIVATE[type] = instance;
|
||||
}
|
||||
|
||||
addTraits(traits) {
|
||||
const allTypes = this.allTypes();
|
||||
for (const type in traits) {
|
||||
this.addTrait(type, traits[type]);
|
||||
}
|
||||
}
|
||||
|
||||
fromJSON(json) {
|
||||
this.addTraits(json);
|
||||
return this;
|
||||
}
|
||||
|
||||
getProperty(property) {
|
||||
if (property in this.actions_PRIVATE) {
|
||||
return this.actions_PRIVATE[property];
|
||||
}
|
||||
if (property in this.properties_PRIVATE) {
|
||||
const instance = this.properties_PRIVATE[property].instance;
|
||||
if (!this.properties_PRIVATE[property].get) {
|
||||
const type = instance.constructor.type();
|
||||
throw new ReferenceError(
|
||||
`Property '${property}' from Trait '${type}' has no getter`
|
||||
);
|
||||
}
|
||||
return instance[property];
|
||||
}
|
||||
}
|
||||
|
||||
hasProperty(property) {
|
||||
if (property in this.actions_PRIVATE) {
|
||||
return true;
|
||||
}
|
||||
if (property in this.properties_PRIVATE) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
hasTrait(type) {
|
||||
return type in this.traits_PRIVATE;
|
||||
}
|
||||
|
||||
hydrate() {
|
||||
const promises = [];
|
||||
for (const type in this.traits_PRIVATE) {
|
||||
const instance = this.traits_PRIVATE[type];
|
||||
promises.push(instance.hydrate());
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
instance(type) {
|
||||
return this.traits_PRIVATE[type];
|
||||
}
|
||||
|
||||
invokeHook(hook, ...args) {
|
||||
const results = {};
|
||||
if (!(hook in this.hooks_PRIVATE)) {
|
||||
return results;
|
||||
}
|
||||
for (const {fn, type} of this.hooks_PRIVATE[hook]) {
|
||||
results[type] = fn(...args);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
invokeHookFlat(hook, ...args) {
|
||||
const invokeResults = this.invokeHook(hook, ...args);
|
||||
const results = [];
|
||||
for (const type in invokeResults) {
|
||||
results.push(invokeResults[type]);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
removeAllTraits() {
|
||||
const types = this.allTypes();
|
||||
this.removeTraits(types);
|
||||
}
|
||||
|
||||
removeTrait(type) {
|
||||
if (!this.hasTrait(type)) {
|
||||
debug(`Tried to remove trait "${type}" when it doesn't exist!`);
|
||||
return;
|
||||
}
|
||||
|
||||
const instance = this.traits_PRIVATE[type];
|
||||
|
||||
const actions = instance.actions();
|
||||
for (const key in actions) {
|
||||
delete this.actions_PRIVATE[key];
|
||||
}
|
||||
|
||||
const hooks = instance.hooks();
|
||||
for (const key in hooks) {
|
||||
delete this.hooks_PRIVATE[key];
|
||||
}
|
||||
|
||||
const Trait = lookupTrait(type);
|
||||
const properties = enumerateProperties(Trait.prototype);
|
||||
for (const key in properties) {
|
||||
delete this.properties_PRIVATE[key];
|
||||
}
|
||||
|
||||
instance.destroy();
|
||||
|
||||
this.state_PRIVATE = this.state_PRIVATE.delete(type);
|
||||
delete this.traits_PRIVATE[type];
|
||||
}
|
||||
|
||||
removeTraits(types) {
|
||||
types.forEach((type) => this.removeTrait(type));
|
||||
}
|
||||
|
||||
setProperty(property, value, receiver) {
|
||||
if (property in this.properties_PRIVATE) {
|
||||
const instance = this.properties_PRIVATE[property].instance;
|
||||
const type = instance.constructor.type();
|
||||
if (!this.properties_PRIVATE[property].set) {
|
||||
throw new ReferenceError(
|
||||
`Property '${property}' from Trait '${type}' has no setter`
|
||||
);
|
||||
}
|
||||
instance[property] = value;
|
||||
this.state_PRIVATE = this.state_PRIVATE.set(type, instance.state);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
state() {
|
||||
return this.state_PRIVATE;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const json = {};
|
||||
for (const type in this.traits_PRIVATE) {
|
||||
json[type] = this.traits_PRIVATE[type].toJSON();
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
}
|
59
packages/entity/traits/mobile.js
Normal file
59
packages/entity/traits/mobile.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
import {compose} from '@avocado/core';
|
||||
import {Vector} from '@avocado/math';
|
||||
|
||||
import {simpleState, Trait} from '../trait';
|
||||
|
||||
const decorate = compose(
|
||||
simpleState('isMobile'),
|
||||
simpleState('speed'),
|
||||
)
|
||||
|
||||
class MobileBase extends Trait {
|
||||
|
||||
static defaultState() {
|
||||
return {
|
||||
isMobile: true,
|
||||
speed: 0,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(entity) {
|
||||
super(entity);
|
||||
this.requestedMovement = [0, 0];
|
||||
}
|
||||
|
||||
actions() {
|
||||
return {
|
||||
requestMovement: (vector) => {
|
||||
if (!this.isMobile) {
|
||||
return;
|
||||
}
|
||||
this.requestedMovement = Vector.scale(
|
||||
Vector.hypotenuse(vector),
|
||||
this.speed
|
||||
);
|
||||
this.entity.emit('movementRequest', this.requestedMovement);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
listeners() {
|
||||
return {
|
||||
tick: (elapsed) => {
|
||||
if (Vector.isZero(this.requestedMovement)) {
|
||||
return;
|
||||
}
|
||||
const requestedMovement = Vector.scale(
|
||||
this.requestedMovement,
|
||||
elapsed
|
||||
);
|
||||
this.requestedMovement = [0, 0];
|
||||
this.entity.x += requestedMovement[0];
|
||||
this.entity.y += requestedMovement[1];
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class Mobile extends decorate(MobileBase) {}
|
32
packages/entity/traits/positioned.js
Normal file
32
packages/entity/traits/positioned.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import {compose} from '@avocado/core';
|
||||
|
||||
import {simpleState, Trait} from '../trait';
|
||||
|
||||
const decorate = compose(
|
||||
simpleState('x'),
|
||||
simpleState('y'),
|
||||
);
|
||||
|
||||
class PositionedBase extends Trait {
|
||||
|
||||
static defaultState() {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
}
|
||||
|
||||
get position() {
|
||||
return [
|
||||
this.state.get('x'),
|
||||
this.state.get('y'),
|
||||
];
|
||||
}
|
||||
|
||||
set position([x, y]) {
|
||||
this.state = this.state.merge({x, y});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class Positioned extends decorate(PositionedBase) {}
|
29
packages/entity/traits/registry.js
Normal file
29
packages/entity/traits/registry.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
// import {registerType} from '@avocado/behavior';
|
||||
|
||||
const traitRegistry = new Map();
|
||||
|
||||
export function registerTrait(Trait) {
|
||||
traitRegistry.set(Trait.type(), Trait);
|
||||
// registerType(`entity:trait:${Trait.type()}`, Trait.contextType());
|
||||
}
|
||||
|
||||
export function hasTrait(type) {
|
||||
return traitRegistry.has(type);
|
||||
}
|
||||
|
||||
export function lookupTrait(type) {
|
||||
return traitRegistry.get(type);
|
||||
}
|
||||
|
||||
// Register core traits.
|
||||
import {Directional} from './directional';
|
||||
registerTrait(Directional);
|
||||
|
||||
import {Existent} from './existent';
|
||||
registerTrait(Existent);
|
||||
|
||||
import {Mobile} from './mobile';
|
||||
registerTrait(Mobile);
|
||||
|
||||
import {Positioned} from './positioned';
|
||||
registerTrait(Positioned);
|
0
packages/graphics/image.js
Normal file
0
packages/graphics/image.js
Normal file
0
packages/graphics/index.js
Normal file
0
packages/graphics/index.js
Normal file
14
packages/graphics/package.json
Normal file
14
packages/graphics/package.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "@avocado/graphics",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"author": "cha0s",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@avocado/core": "1.x",
|
||||
"@avocado/math": "1.x",
|
||||
"@avocado/mixins": "1.x",
|
||||
"debug": "^3.1.0",
|
||||
"immutable": "4.0.0-rc.12"
|
||||
}
|
||||
}
|
123
packages/input/index.js
Normal file
123
packages/input/index.js
Normal file
|
@ -0,0 +1,123 @@
|
|||
import * as I from 'immutable';
|
||||
|
||||
import {compose} from '@avocado/core';
|
||||
import {EventEmitter} from '@avocado/mixins';
|
||||
|
||||
const decorate = compose(
|
||||
EventEmitter,
|
||||
);
|
||||
|
||||
class ActionRegistryBase {
|
||||
|
||||
static normalizeKey(key) {
|
||||
return key.toLowerCase();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// Track events.
|
||||
this.target = undefined;
|
||||
this.mapActionToKey = {};
|
||||
this.mapKeyToAction = {};
|
||||
// Track actions.
|
||||
this._state = I.Set();
|
||||
// Handle lame OS input event behavior. See: https://mzl.la/2Ob0WQE
|
||||
this.keysDown = {};
|
||||
this.keyUpDelays = {};
|
||||
// Bind event handlers.
|
||||
this.onBlur = this.onBlur.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
this.onKeyUp = this.onKeyUp.bind(this);
|
||||
}
|
||||
|
||||
actionForKey(key) {
|
||||
return this.mapKeyToAction[key];
|
||||
}
|
||||
|
||||
listen(target = window.document) {
|
||||
// Only listen once.
|
||||
if (this.target) {
|
||||
return;
|
||||
}
|
||||
this.target = target;
|
||||
this.target.addEventListener('blur', this.onBlur);
|
||||
this.target.addEventListener('keydown', this.onKeyDown);
|
||||
this.target.addEventListener('keyup', this.onKeyUp);
|
||||
}
|
||||
|
||||
keyForAction(action) {
|
||||
return this.mapActionToKey[action];
|
||||
}
|
||||
|
||||
mapKeysToActions(map) {
|
||||
for (const key in map) {
|
||||
const action = map[key];
|
||||
this.mapKeyToAction[key] = action;
|
||||
this.mapActionToKey[action] = key;
|
||||
}
|
||||
}
|
||||
|
||||
onBlur(event) {
|
||||
event = event || window.event;
|
||||
this.setAllKeysUp();
|
||||
}
|
||||
|
||||
onKeyDown(event) {
|
||||
event = event || window.event;
|
||||
const key = this.constructor.normalizeKey(event.key);
|
||||
if (this.keysDown[key]) {
|
||||
if (this.keyUpDelays[key]) {
|
||||
clearTimeout(this.keyUpDelays[key]);
|
||||
delete this.keyUpDelays[key];
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.keysDown[key] = true;
|
||||
if (this.mapKeyToAction[key]) {
|
||||
const action = this.mapKeyToAction[key];
|
||||
this._state = this._state.add(action);
|
||||
this.emit('actionStart', action);
|
||||
}
|
||||
}
|
||||
|
||||
onKeyUp(event) {
|
||||
event = event || window.event;
|
||||
const key = this.constructor.normalizeKey(event.key);
|
||||
this.keyUpDelays[key] = setTimeout(() => {
|
||||
delete this.keyUpDelays[key];
|
||||
delete this.keysDown[key];
|
||||
if (this.mapKeyToAction[key]) {
|
||||
const action = this.mapKeyToAction[key];
|
||||
this._state = this._state.delete(action);
|
||||
this.emit('actionStop', action);
|
||||
}
|
||||
}, 20);
|
||||
}
|
||||
|
||||
setAllKeysUp() {
|
||||
this.keysDown = {};
|
||||
for (const key in this.keyUpDelays) {
|
||||
const handle = this.keyUpDelays[key];
|
||||
clearTimeout(handle);
|
||||
}
|
||||
this.keyUpDelays = {};
|
||||
this._state = I.Set();
|
||||
}
|
||||
|
||||
state() {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
stopListening() {
|
||||
this.setAllKeysUp();
|
||||
if (!this.target) {
|
||||
return;
|
||||
}
|
||||
this.target.removeEventListener('blur', this.onBlur);
|
||||
this.target.removeEventListener('keydown', this.onKeyDown);
|
||||
this.target.removeEventListener('keyup', this.onKeyUp);
|
||||
this.target = undefined;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class ActionRegistry extends decorate(ActionRegistryBase) {};
|
13
packages/input/package.json
Normal file
13
packages/input/package.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "@avocado/input",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"author": "cha0s",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@avocado/core": "1.x",
|
||||
"@avocado/mixins": "1.x",
|
||||
"debug": "^3.1.0",
|
||||
"immutable": "4.0.0-rc.12"
|
||||
}
|
||||
}
|
4
packages/math/index.js
Normal file
4
packages/math/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
// export * as Matrix from './matrix';
|
||||
// export * as Rectangle from './rectangle';
|
||||
import * as Vector from './vector';
|
||||
export {Vector};
|
31
packages/math/matrix.js
Normal file
31
packages/math/matrix.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
// Matrix operations.
|
||||
|
||||
// **Matrix** is a utility class to help with matrix operations. A
|
||||
// matrix is implemented as an n-element array. Data is stored in row-major
|
||||
// order.
|
||||
|
||||
export copy = (matrix) => matrix.map((row) => [...row])
|
||||
|
||||
export equals = (l, r) ->
|
||||
|
||||
return false unless l.length is r.length
|
||||
return true if l.length is 0
|
||||
return false unless l[0].length is r[0].length
|
||||
|
||||
for lrow, y in l
|
||||
rrow = r[y]
|
||||
for lindex, x in lrow
|
||||
unless lindex is rrow[x]
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
export size = (matrix) ->
|
||||
|
||||
return 0 if 0 is matrix.length
|
||||
return matrix.length * matrix[0].length
|
||||
|
||||
export sizeVector = (matrix) ->
|
||||
|
||||
return [0, 0] if 0 is matrix.length
|
||||
return [matrix[0].length, matrix.length]
|
29
packages/math/matrix.spec.coffee
Normal file
29
packages/math/matrix.spec.coffee
Normal file
|
@ -0,0 +1,29 @@
|
|||
|
||||
import * as Matrix from './matrix'
|
||||
|
||||
describe 'Matrix', ->
|
||||
|
||||
it 'can inspect size', ->
|
||||
|
||||
matrix = [[0, 0], [0, 0], [0, 0], [0, 0]]
|
||||
|
||||
expect(Matrix.size matrix).toBe 8
|
||||
expect(Matrix.sizeVector matrix).toEqual [2, 4]
|
||||
|
||||
it 'can test equality', ->
|
||||
|
||||
l = [[0, 0], [0, 0], [0, 0], [0, 0]]
|
||||
r = [[0, 0], [0, 0], [0, 0], [0, 0]]
|
||||
|
||||
expect(Matrix.equals l, r).toBe true
|
||||
|
||||
it 'can make deep copies', ->
|
||||
|
||||
matrix = [[1], [2], [3]]
|
||||
matrix2 = Matrix.copy matrix
|
||||
|
||||
expect(matrix).toEqual matrix2
|
||||
|
||||
matrix[0][0] = 4
|
||||
|
||||
expect(matrix).not.toEqual matrix2
|
7
packages/math/package.json
Normal file
7
packages/math/package.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@avocado/math",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"author": "cha0s",
|
||||
"license": "MIT"
|
||||
}
|
167
packages/math/rectangle/index.coffee
Normal file
167
packages/math/rectangle/index.coffee
Normal file
|
@ -0,0 +1,167 @@
|
|||
# Rectangle operations.
|
||||
|
||||
# **Rectangle** is a utility class to help with rectangle operations. A
|
||||
# rectangle is implemented as a 4-element array. Element 0 is *x*, element
|
||||
# 1 is *y*, element 2 is *width* and element 3 is *height*.
|
||||
|
||||
import * as Vector from '../vector'
|
||||
|
||||
# Check if a rectangle intersects with another rectangle.
|
||||
#
|
||||
# avocado> Rectangle.intersects [0, 0, 16, 16], [8, 8, 24, 24]
|
||||
# true
|
||||
#
|
||||
# avocado> Rectangle.intersects [0, 0, 16, 16], [16, 16, 32, 32]
|
||||
# false
|
||||
export intersects = (l, r) ->
|
||||
|
||||
return false if l[0] >= r[0] + r[2]
|
||||
return false if r[0] >= l[0] + l[2]
|
||||
return false if l[1] >= r[1] + r[3]
|
||||
return false if r[1] >= l[1] + l[3]
|
||||
|
||||
return true
|
||||
|
||||
# Check if a rectangle is touching a vector.
|
||||
#
|
||||
# avocado> Rectangle.isTouching [0, 0, 16, 16], [0, 0]
|
||||
# true
|
||||
#
|
||||
# avocado> Rectangle.intersects [0, 0, 16, 16], [16, 16]
|
||||
# false
|
||||
export isTouching = (r, v) ->
|
||||
|
||||
return false if v[0] < r[0]
|
||||
return false if v[1] < r[1]
|
||||
return false if v[0] >= r[0] + r[2]
|
||||
return false if v[1] >= r[1] + r[3]
|
||||
|
||||
return true
|
||||
|
||||
# Compose a rectangle from a position vector and a size vector.
|
||||
#
|
||||
# avocado> Rectangle.compose [0, 0], [16, 16]
|
||||
# [0, 0, 16, 16]
|
||||
export compose = (l, r) -> [l[0], l[1], r[0], r[1]]
|
||||
|
||||
# Make a deep copy of the rectangle.
|
||||
#
|
||||
# avocado> rectangle = [0, 0, 16, 16]
|
||||
# avocado> rectangle is Rectangle.copy rectangle
|
||||
# false
|
||||
export copy = (r) -> [r[0], r[1], r[2], r[3]]
|
||||
|
||||
# Convert a rectangle to an object. If you *useShortKeys*, The width and
|
||||
# height keys will be named w and h, respectively.
|
||||
#
|
||||
# avocado> Rectangle.toObject [3, 4, 5, 6]
|
||||
# {x: 3, y: 4, width: 5, height: 6}
|
||||
#
|
||||
# avocado> Rectangle.toObject [3, 4, 5, 6], true
|
||||
# {x: 3, y: 4, w: 5, h: 6}
|
||||
export toObject = (r, useShortKeys = false) ->
|
||||
|
||||
whKeys = if useShortKeys then ['w', 'h'] else ['width', 'height']
|
||||
|
||||
O = x: r[0], y: r[1]
|
||||
O[whKeys[0]] = r[2]
|
||||
O[whKeys[1]] = r[3]
|
||||
return O
|
||||
|
||||
export fromObject = (O) -> [O.x, O.y, O.width, O.height]
|
||||
|
||||
# Returns the position of a rectangle.
|
||||
#
|
||||
# avocado> Rectangle.position [8, 8, 16, 16]
|
||||
# [8, 8]
|
||||
export position = (r) -> [r[0], r[1]]
|
||||
|
||||
# Returns the size of a rectangle.
|
||||
#
|
||||
# avocado> Rectangle.size [8, 8, 16, 16]
|
||||
# [16, 16]
|
||||
export size = (r) -> [r[2], r[3]]
|
||||
|
||||
# Compute the intersection rectangle of two rectangles.
|
||||
#
|
||||
# avocado> Rectangle.intersection [0, 0, 16, 16], [8, 8, 24, 24]
|
||||
# [8, 8, 8, 8]
|
||||
export intersection = (l, r) ->
|
||||
|
||||
return [0, 0, 0, 0] unless intersects l, r
|
||||
|
||||
x = Math.max l[0], r[0]
|
||||
y = Math.max l[1], r[1]
|
||||
|
||||
lx2 = l[0] + l[2]
|
||||
rx2 = r[0] + r[2]
|
||||
ly2 = l[1] + l[3]
|
||||
ry2 = r[1] + r[3]
|
||||
|
||||
w = (if lx2 <= rx2 then lx2 else rx2) - x
|
||||
h = (if ly2 <= ry2 then ly2 else ry2) - y
|
||||
|
||||
return [x, y, w, h]
|
||||
|
||||
# Returns a rectangle translated along the [*x*, *y*] axis of a vector.
|
||||
#
|
||||
# avocado> Rectangle.translated [0, 0, 16, 16], [8, 8]
|
||||
# [8, 8, 16, 16]
|
||||
export translated = (r, v) -> compose(
|
||||
Vector.add v, position r
|
||||
size r
|
||||
)
|
||||
|
||||
# Checks if a rectangle is null. A null rectangle is defined by having any
|
||||
# 0-length axis.
|
||||
#
|
||||
# avocado> Rectangle.isNull [0, 0, 1, 1]
|
||||
# false
|
||||
#
|
||||
# avocado> Rectangle.isNull [0, 0, 1, 0]
|
||||
# true
|
||||
export isNull = (r) ->
|
||||
|
||||
return true unless r?
|
||||
return true unless r.length is 4
|
||||
|
||||
return Vector.isNull size r
|
||||
|
||||
# Check whether a rectangle equals another rectangle.
|
||||
#
|
||||
# avocado> Rectangle.equals [0, 0, 0, 0], [0, 0, 0, 1]
|
||||
# false
|
||||
#
|
||||
# avocado> Rectangle.equals [0, 0, 0, 0], [0, 0, 0, 0]
|
||||
# true
|
||||
export equals = (l, r) ->
|
||||
|
||||
return l[0] is r[0] and l[1] is r[1] and l[2] is r[2] and l[3] is r[3]
|
||||
|
||||
# Returns a rectangle that is the united area of two rectangles.
|
||||
#
|
||||
# avocado> Rectangle.united [0, 0, 4, 4], [4, 4, 8, 8]
|
||||
# [0, 0, 12, 12]
|
||||
export united = (l, r) ->
|
||||
|
||||
return r if isNull l
|
||||
return l if isNull r
|
||||
|
||||
x = Math.min l[0], r[0]
|
||||
y = Math.min l[1], r[1]
|
||||
x2 = Math.max l[0] + l[2], r[0] + r[2]
|
||||
y2 = Math.max l[1] + l[3], r[1] + r[3]
|
||||
|
||||
return [x, y, x2 - x, y2 - y]
|
||||
|
||||
# Round the position and size of a rectangle.
|
||||
#
|
||||
# avocado> Rectangle.round [3.14, 4.70, 5.32, 1.8]
|
||||
# [3, 5, 5, 2]
|
||||
export round = (r) -> r.map Math.round
|
||||
|
||||
# Floor the position and size of a rectangle.
|
||||
#
|
||||
# avocado> Rectangle.floor [3.14, 4.70, 5.32, 1.8]
|
||||
# [3, 4, 5, 1]
|
||||
export floor = (r) -> r.map Math.floor
|
70
packages/math/rectangle/index.spec.coffee
Normal file
70
packages/math/rectangle/index.spec.coffee
Normal file
|
@ -0,0 +1,70 @@
|
|||
|
||||
import * as Rectangle from './rectangle'
|
||||
|
||||
describe 'Rectangle', ->
|
||||
|
||||
it 'can calculate intersections', ->
|
||||
|
||||
expect(Rectangle.intersects [0, 0, 16, 16], [8, 8, 24, 24]).toBe true
|
||||
expect(Rectangle.intersects [0, 0, 16, 16], [16, 16, 32, 32]).toBe false
|
||||
|
||||
expect(Rectangle.isTouching [0, 0, 16, 16], [0, 0]).toBe true
|
||||
expect(Rectangle.isTouching [0, 0, 16, 16], [16, 16]).toBe false
|
||||
|
||||
expect(Rectangle.intersection(
|
||||
[0, 0, 16, 16], [8, 8, 24, 24]
|
||||
)).toEqual [8, 8, 8, 8]
|
||||
|
||||
expect(Rectangle.united [0, 0, 4, 4], [4, 4, 8, 8]).toEqual [0, 0, 12, 12]
|
||||
|
||||
it 'can compose and decompose', ->
|
||||
|
||||
rectangle = Rectangle.compose [0, 0], [16, 16]
|
||||
|
||||
expect(Rectangle.equals rectangle, [0, 0, 16, 16]).toBe true
|
||||
|
||||
expect(Rectangle.position rectangle).toEqual [0, 0]
|
||||
expect(Rectangle.size rectangle).toEqual [16, 16]
|
||||
|
||||
it 'can make a deep copy', ->
|
||||
|
||||
rectangle = [0, 0, 16, 16]
|
||||
rectangle2 = Rectangle.copy rectangle
|
||||
|
||||
expect(Rectangle.equals rectangle, rectangle2).toBe true
|
||||
|
||||
rectangle[0] = 6
|
||||
|
||||
expect(Rectangle.equals rectangle, rectangle2).toBe false
|
||||
|
||||
it 'can convert to an object', ->
|
||||
|
||||
rectangle = [0, 0, 16, 16]
|
||||
|
||||
expect(Rectangle.toObject rectangle).toEqual(
|
||||
x: 0, y: 0, width: 16, height: 16
|
||||
)
|
||||
|
||||
expect(Rectangle.toObject rectangle, true).toEqual(
|
||||
x: 0, y: 0, w: 16, h: 16
|
||||
)
|
||||
|
||||
it 'can translate by vector', ->
|
||||
|
||||
expect(Rectangle.translated [0, 0, 16, 16], [8, 8]).toEqual [8, 8, 16, 16]
|
||||
|
||||
it 'can check for null', ->
|
||||
|
||||
expect(Rectangle.isNull null).toBe true
|
||||
expect(Rectangle.isNull 3).toBe true
|
||||
expect(Rectangle.isNull [1]).toBe true
|
||||
expect(Rectangle.isNull [1, 1]).toBe true
|
||||
expect(Rectangle.isNull [1, 1, 1]).toBe true
|
||||
expect(Rectangle.isNull [1, 1, 1, 1, 1]).toBe true
|
||||
expect(Rectangle.isNull [0, 0, 1, 1]).toBe false
|
||||
expect(Rectangle.isNull [0, 0, 1, 0]).toBe true
|
||||
|
||||
it 'can do mathematical operations', ->
|
||||
|
||||
expect(Rectangle.round [3.14, 4.70, 5.32, 1.8]).toEqual [3, 5, 5, 2]
|
||||
expect(Rectangle.floor [3.14, 4.70, 5.32, 1.8]).toEqual [3, 4, 5, 1]
|
40
packages/math/rectangle/mixin.coffee
Normal file
40
packages/math/rectangle/mixin.coffee
Normal file
|
@ -0,0 +1,40 @@
|
|||
import {Mixin, Property} from '@avocado/composition'
|
||||
import {setterName} from '@avocado/string'
|
||||
|
||||
import {Vector} from '../vector'
|
||||
import {Rectangle_} from './index'
|
||||
|
||||
export RectangleMixin = (
|
||||
rectangle = 'rectangle'
|
||||
x = 'x'
|
||||
y = 'y'
|
||||
width = 'width'
|
||||
height = 'height'
|
||||
position = 'position'
|
||||
size = 'size'
|
||||
meta = {}
|
||||
) -> (Superclass) ->
|
||||
|
||||
setPosition = setterName position
|
||||
setSize = setterName size
|
||||
|
||||
class Rectangle extends Mixin(Superclass).with(
|
||||
|
||||
position, x, y, meta[position]
|
||||
size, width, height, meta[size]
|
||||
|
||||
Property rectangle, Object.assign {
|
||||
|
||||
get: -> Rectangle.compose this[position], this[size]
|
||||
|
||||
set: (rectangle) ->
|
||||
|
||||
this[setPosition] Rectangle.position rectangle
|
||||
this[setSize] Rectangle.size rectangle
|
||||
|
||||
return
|
||||
|
||||
eq: (l, r) -> Rectangle_.equals l, r
|
||||
|
||||
}, meta
|
||||
)
|
350
packages/math/vector/index.js
Normal file
350
packages/math/vector/index.js
Normal file
|
@ -0,0 +1,350 @@
|
|||
|
||||
export const SQRT_2_2 = Math.sqrt(2) / 2;
|
||||
|
||||
export const EIGHTH_PI = Math.PI * 0.125;
|
||||
export const QUARTER_PI = EIGHTH_PI * 2;
|
||||
export const HALF_PI = QUARTER_PI * 2;
|
||||
export const TWO_PI = Math.PI * 2;
|
||||
|
||||
// export function {VectorMixin as Mixin} from './mixin'
|
||||
|
||||
// Scale a vector. This multiplies *x* and *y* by **k**.
|
||||
//
|
||||
// avocado> Vector.scale [.5, 1.5], 2
|
||||
// [1, 3]
|
||||
export function scale(v, k) {
|
||||
return [v[0] * k, v[1] * k];
|
||||
}
|
||||
|
||||
// Add two vectors.
|
||||
//
|
||||
// avocado> Vector.add [1, 2], [1, 1]
|
||||
// [2, 3]
|
||||
export function add(l, r) {
|
||||
return [l[0] + r[0], l[1] + r[1]];
|
||||
}
|
||||
|
||||
// Subtract two vectors.
|
||||
//
|
||||
// avocado> Vector.sub [9, 5], [5, 2]
|
||||
// [4, 3]
|
||||
export function sub(l, r) {
|
||||
return [l[0] - r[0], l[1] - r[1]];
|
||||
}
|
||||
|
||||
// Multiply two vectors.
|
||||
//
|
||||
// avocado> Vector.mul [3, 5], [5, 5]
|
||||
// [15, 25]
|
||||
export function mul(l, r) {
|
||||
[l[0] * r[0], l[1] * r[1]];
|
||||
}
|
||||
|
||||
// Divide two vectors.
|
||||
//
|
||||
// avocado> Vector.div [15, 5], [5, 5]
|
||||
// [3, 1]
|
||||
export function div(l, r) {
|
||||
return [l[0] / r[0], l[1] / r[1]];
|
||||
}
|
||||
|
||||
// Modulo divide two vectors.
|
||||
//
|
||||
// avocado> Vector.mod [13, 6], [5, 5]
|
||||
// [3, 1]
|
||||
export function mod(l, r) {
|
||||
return [l[0] % r[0], l[1] % r[1]];
|
||||
}
|
||||
|
||||
// Get the cartesian distance between two point vectors.
|
||||
//
|
||||
// avocado> Vector.cartesianDistance [0, 0], [1, 1]
|
||||
// 1.4142135623730951
|
||||
export function cartesianDistance (l, r) {
|
||||
const xd = l[0] - r[0];
|
||||
const yd = l[1] - r[1];
|
||||
return Math.sqrt(xd * xd + yd * yd);
|
||||
}
|
||||
|
||||
// Get the minimum values from two vectors.
|
||||
//
|
||||
// avocado> Vector.min [-10, 10], [0, 0]
|
||||
// [-10, 0]
|
||||
export function min(l, r) {
|
||||
return [Math.min(l[0], r[0]), Math.min(l[1], r[1])];
|
||||
}
|
||||
|
||||
// Get the maximum values from two vectors.
|
||||
//
|
||||
// avocado> Vector.max [-10, 10], [0, 0]
|
||||
// [0, 10]
|
||||
export function max(l, r) {
|
||||
return [Math.max(l[0], r[0]), Math.max(l[1], r[1])];
|
||||
}
|
||||
|
||||
// Clamp a vector's axes using a min vector and a max vector.
|
||||
//
|
||||
// avocado> Vector.clamp [-10, 10], [0, 0], [5, 5]
|
||||
// [0, 5]
|
||||
export function clamp(v, min_, max_) {
|
||||
return min(max_, max(min_, v));
|
||||
}
|
||||
|
||||
// Returns a deep copy of the vector.
|
||||
//
|
||||
// avocado> vector = [0, 0]
|
||||
// avocado> otherVectory Vector.copy vector
|
||||
// avocado> vector is otherVector
|
||||
// false
|
||||
export function copy(v) {
|
||||
return [v[0], v[1]];
|
||||
}
|
||||
|
||||
// Check whether a vector equals another vector.
|
||||
//
|
||||
// avocado> Vector.equals [4, 4], [5, 4]
|
||||
// false
|
||||
//
|
||||
// avocado> Vector.equals [4, 4], [4, 4]
|
||||
// true
|
||||
export function equals(l, r) {
|
||||
return l[0] === r[0] && l[1] === r[1];
|
||||
}
|
||||
|
||||
// Checks whether a vector is [0, 0].
|
||||
//
|
||||
// avocado> Vector.zero [0, 0]
|
||||
// true
|
||||
//
|
||||
// avocado> Vector.zero [0, 1]
|
||||
// false
|
||||
export function isZero(v) {
|
||||
return v[0] === 0 && v[1] === 0;
|
||||
}
|
||||
|
||||
// Round both axes of a vector.
|
||||
//
|
||||
// avocado> Vector.round [3.14, 4.70]
|
||||
// [3, 5]
|
||||
export function round(v) {
|
||||
return [Math.round(v[0]), Math.round(v[1])];
|
||||
}
|
||||
|
||||
// Get the dot product of two vectors.
|
||||
//
|
||||
// avocado> Vector.dot [2, 3], [4, 5]
|
||||
// 23
|
||||
export function dot(l, r) {
|
||||
return l[0] * r[0] + l[1] * r[1];
|
||||
}
|
||||
|
||||
// Get a hypotenuse unit vector. If an origin vector is passed in, the
|
||||
// hypotenuse is derived from the distance to the origin.
|
||||
//
|
||||
// avocado> Vector.hypotenuse [5, 5], [6, 7]
|
||||
// [-0.4472135954999579, -0.8944271909999159]
|
||||
//
|
||||
// avocado> Vector.hypotenuse [.5, .7]
|
||||
// [0.5812381937190965, 0.813733471206735]
|
||||
export function hypotenuse(unitOrDestination, origin) {
|
||||
let distanceOrUnit = origin ?
|
||||
sub(unitOrDestination, origin)
|
||||
:
|
||||
unitOrDestination;
|
||||
const dp = dot(distanceOrUnit, distanceOrUnit);
|
||||
if (0 === dp) {
|
||||
return [0, 0];
|
||||
}
|
||||
const hypotenuse = scale(distanceOrUnit, 1 / Math.sqrt(dp));
|
||||
// Don't let NaN poison our equations.
|
||||
return [
|
||||
NaN === hypotenuse[0] ? 0 : hypotenuse[0],
|
||||
NaN === hypotenuse[1] ? 0 : hypotenuse[1],
|
||||
];
|
||||
}
|
||||
|
||||
// Get the absolute values of the axes of a vector.
|
||||
//
|
||||
// avocado> Vector.abs [23, -5.20]
|
||||
// [23, 5.20]
|
||||
export function abs(v) {
|
||||
return [Math.abs(v[0]), Math.abs(v[1])];
|
||||
}
|
||||
|
||||
// Floor both axes of a vector.
|
||||
//
|
||||
// avocado> Vector.floor [3.14, 4.70]
|
||||
// [3, 4]
|
||||
export function floor(v) {
|
||||
return [Math.floor(v[0]), Math.floor(v[1])];
|
||||
}
|
||||
|
||||
// Ceiling both axes of a vector.
|
||||
//
|
||||
// avocado> Vector.floor [3.14, 4.70]
|
||||
// [3, 4]
|
||||
export function ceil(v) {
|
||||
return [Math.ceil(v[0]), Math.ceil(v[1])];
|
||||
}
|
||||
|
||||
// Get the area a vector.
|
||||
//
|
||||
// avocado> Vector.area [3, 6]
|
||||
// 18
|
||||
export function area(v) {
|
||||
return v[0] * v[1];
|
||||
}
|
||||
|
||||
// Checks whether a vector is null. A vector is null if either axis is 0.
|
||||
// The algorithm prefers horizontal directions to vertical; if you move
|
||||
// up-right or down-right you'll face right.
|
||||
//
|
||||
// avocado> Vector.isNull [1, 0]
|
||||
// true
|
||||
//
|
||||
// avocado> Vector.isNull [1, 1]
|
||||
// false
|
||||
export function isNull(v) {
|
||||
if (!v) {
|
||||
return true;
|
||||
}
|
||||
if (2 !== v.length) {
|
||||
return true;
|
||||
}
|
||||
return 0 === v[0] || 0 === v[1];
|
||||
}
|
||||
|
||||
export function overshot(position, hypotenuse, destination) {
|
||||
const overshot = [false, false];
|
||||
for (const i = 0; i < 2; ++i) {
|
||||
if (hypotenuse[i] < 0) {
|
||||
if (position[i] < destination[i]) {
|
||||
overshot[i] = true;
|
||||
}
|
||||
}
|
||||
else if (hypotenuse[i] > 0) {
|
||||
if (position[i] > destination[i]) {
|
||||
overshot[i] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return overshot;
|
||||
}
|
||||
|
||||
// Convert a vector to a 4-direction. A 4-direction is:
|
||||
//
|
||||
// * 0: Up
|
||||
// * 1: Right
|
||||
// * 2: Down
|
||||
// * 3: Left
|
||||
//
|
||||
// avocado> Vector.toDirection4 [0, 1]
|
||||
// 2
|
||||
//
|
||||
// avocado> Vector.toDirection4 [1, 0]
|
||||
// 1
|
||||
export function toDirection4(vector) {
|
||||
vector = hypotenuse(vector);
|
||||
const x = Math.abs(vector[0]) - SQRT_2_2;
|
||||
if (x > 0 && x < SQRT_2_2) {
|
||||
return vector[0] > 0 ? 1 : 3;
|
||||
}
|
||||
else {
|
||||
return vector[1] > 0 ? 2 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert a vector to an 8-direction. An 8-direction is:
|
||||
//
|
||||
// * 0: Up
|
||||
// * 1: Right
|
||||
// * 2: Down
|
||||
// * 3: Left
|
||||
// * 4: Up-Right
|
||||
// * 5: Down-Right
|
||||
// * 6: Down-Left
|
||||
// * 7: Up-Left
|
||||
//
|
||||
// avocado> Vector.toDirection8 [1, 1]
|
||||
// 5
|
||||
//
|
||||
// avocado> Vector.toDirection8 [1, 0]
|
||||
// 1
|
||||
export function toDirection8(v) {
|
||||
v = hypotenuse(v);
|
||||
// Orient radians.
|
||||
let rad = (TWO_PI + Math.atan2(v[1], v[0])) % TWO_PI;
|
||||
rad = (rad + HALF_PI) % TWO_PI;
|
||||
// Truncate.
|
||||
rad = Math.floor(rad * 100000) / 100000;
|
||||
const stepStart = TWO_PI - EIGHTH_PI;
|
||||
// Clockwise direction map.
|
||||
const directions = [0, 4, 1, 5, 2, 6, 3, 7];
|
||||
for (const direction of directions) {
|
||||
let stepEnd = (stepStart + QUARTER_PI) % TWO_PI
|
||||
stepEnd = Math.floor(stepEnd * 100000) / 100000;
|
||||
if (rad >= stepStart && rad < stepEnd) {
|
||||
return direction;
|
||||
}
|
||||
stepStart = stepEnd;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Convert a vector to a *directionCount*-direction.
|
||||
//
|
||||
// avocado> Vector.toDirection [0, 1], 4
|
||||
// 2
|
||||
export function toDirection(vector, directionCount) {
|
||||
switch (directionCount) {
|
||||
case 1: return 0;
|
||||
case 4: return toDirection4(vector);
|
||||
case 8: return toDirection8(vector);
|
||||
default:
|
||||
throw new Error `Unsupported conversion of vector to ${
|
||||
directionCount
|
||||
}-direction.`;
|
||||
}
|
||||
}
|
||||
|
||||
export function fromDirection(direction) {
|
||||
switch (direction) {
|
||||
case 0: return [0, -1];
|
||||
case 1: return [1, 0];
|
||||
case 2: return [0, 1];
|
||||
case 3: return [-1, 0];
|
||||
case 4: return hypotenuse([1, -1]);
|
||||
case 5: return hypotenuse([1, 1]);
|
||||
case 6: return hypotenuse([-1, 1]);
|
||||
case 7: return hypotenuse([-1, -1]);
|
||||
}
|
||||
}
|
||||
|
||||
export function interpolate(ultimate, actual, easing = 0) {
|
||||
if (0 === easing) {
|
||||
return ultimate;
|
||||
}
|
||||
const distance = cartesianDistance(ultimate, actual);
|
||||
if (0 === distance) {
|
||||
return ultimate;
|
||||
}
|
||||
return add(
|
||||
actual,
|
||||
scale(
|
||||
hypotenuse(ultimate, actual),
|
||||
distance / easing
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function fromObject(object) {
|
||||
return [object.x, object.y];
|
||||
}
|
||||
|
||||
// Convert a vector to an object.
|
||||
//
|
||||
// avocado> Vector.toObject [3, 4]
|
||||
// {x: 3, y: 4}
|
||||
export function toObject(v) {
|
||||
return {x: v[0], y: v[1]};
|
||||
}
|
85
packages/math/vector/index.spec.coffee
Normal file
85
packages/math/vector/index.spec.coffee
Normal file
|
@ -0,0 +1,85 @@
|
|||
|
||||
import {Vector} from '@avocado/math'
|
||||
|
||||
describe 'Vector', ->
|
||||
|
||||
it 'can do mathematical operations', ->
|
||||
|
||||
expect(Vector.scale [.5, 1.5], 2).toEqual [1, 3]
|
||||
|
||||
expect(Vector.add [1, 2], [1, 1]).toEqual [2, 3]
|
||||
|
||||
expect(Vector.sub [9, 5], [5, 2]).toEqual [4, 3]
|
||||
|
||||
expect(Vector.mul [3, 5], [5, 5]).toEqual [15, 25]
|
||||
|
||||
expect(Vector.div [15, 5], [5, 5]).toEqual [3, 1]
|
||||
|
||||
expect(Vector.mod [13, 6], [5, 5]).toEqual [3, 1]
|
||||
|
||||
expect(Vector.cartesianDistance [0, 0], [1, 1]).toBe Math.sqrt 2
|
||||
|
||||
expect(Vector.min [-10, 10], [0, 0]).toEqual [-10, 0]
|
||||
|
||||
expect(Vector.max [-10, 10], [0, 0]).toEqual [0, 10]
|
||||
|
||||
expect(Vector.clamp [-10, 10], [0, 0], [5, 5]).toEqual [0, 5]
|
||||
|
||||
expect(Vector.round [3.14, 4.70]).toEqual [3, 5]
|
||||
|
||||
expect(Vector.dot [2, 3], [4, 5]).toEqual 23
|
||||
|
||||
expect(Vector.hypotenuse [5, 5], [6, 7]).toEqual [
|
||||
-0.4472135954999579, -0.8944271909999159
|
||||
]
|
||||
|
||||
expect(Vector.hypotenuse [.5, .7]).toEqual [
|
||||
0.5812381937190965, 0.813733471206735
|
||||
]
|
||||
|
||||
expect(Vector.abs [23, -5.20]).toEqual [23, 5.20]
|
||||
|
||||
expect(Vector.floor [3.14, 4.70]).toEqual [3, 4]
|
||||
|
||||
expect(Vector.area [3, 6]).toBe 18
|
||||
|
||||
it 'can deep copy', ->
|
||||
|
||||
vector = [0, 0]
|
||||
vector2 = Vector.copy vector
|
||||
|
||||
expect(Vector.equals vector, vector2).toBe true
|
||||
|
||||
vector[0] = 1
|
||||
|
||||
expect(Vector.equals vector, vector2).toBe false
|
||||
|
||||
it 'can test for 0 or NULL', ->
|
||||
|
||||
expect(Vector.isZero [0, 0]).toBe true
|
||||
expect(Vector.isZero [1, 0]).toBe false
|
||||
|
||||
expect(Vector.isNull [0, 1]).toBe true
|
||||
expect(Vector.isNull [1, 1]).toBe false
|
||||
|
||||
expect(Vector.isNull null).toBe true
|
||||
expect(Vector.isNull [1]).toBe true
|
||||
expect(Vector.isNull [1, 1, 1]).toBe true
|
||||
|
||||
it 'can convert to/from directions', ->
|
||||
|
||||
expect(Vector.toDirection4 [0, 1]).toBe 2
|
||||
expect(Vector.toDirection4 [1, 0]).toBe 1
|
||||
|
||||
expect(Vector.toDirection8 [1, 1]).toBe 5
|
||||
expect(Vector.toDirection8 [1, 0]).toBe 1
|
||||
|
||||
expect(Vector.toDirection [0, 1], 4).toBe 2
|
||||
|
||||
for i in [0...8]
|
||||
vector = Vector.fromDirection i
|
||||
expect(i).toBe Vector.toDirection vector, 8
|
||||
|
||||
it 'can convert to object', ->
|
||||
|
||||
expect(Vector.toObject [0, 16]).toEqual x: 0, y: 16
|
32
packages/math/vector/mixin.coffee
Normal file
32
packages/math/vector/mixin.coffee
Normal file
|
@ -0,0 +1,32 @@
|
|||
import {Mixin, Property} from '@avocado/composition'
|
||||
import {Vector as Vector_} from '@avocado/math'
|
||||
import {setterName} from '@avocado/string'
|
||||
|
||||
export VectorMixin = (
|
||||
vector = 'vector', x = 'x', y = 'y', meta = {}
|
||||
) -> (Superclass) ->
|
||||
|
||||
setX = setterName x
|
||||
setY = setterName y
|
||||
|
||||
Base = Mixin(Superclass).with(
|
||||
|
||||
Property x, meta[x] ? {}
|
||||
Property y, meta[y] ? {}
|
||||
|
||||
Property vector, Object.assign {
|
||||
|
||||
set: (vector) ->
|
||||
|
||||
this[setX] vector[0]
|
||||
this[setY] vector[1]
|
||||
|
||||
return
|
||||
|
||||
get: -> [@[x](), @[y]()]
|
||||
|
||||
eq: (l, r) -> Vector_.equals l, r
|
||||
|
||||
}, meta
|
||||
)
|
||||
class Vector extends Base
|
36
packages/math/vector/mixin.spec.coffee
Normal file
36
packages/math/vector/mixin.spec.coffee
Normal file
|
@ -0,0 +1,36 @@
|
|||
import {EventEmitter, MixinOf} from '@avocado/composition'
|
||||
|
||||
import {Vector} from '@avocado/math'
|
||||
|
||||
describe 'Vector.Mixin', ->
|
||||
|
||||
O = null
|
||||
spy = null
|
||||
|
||||
beforeEach ->
|
||||
|
||||
O = new class extends MixinOf(
|
||||
EventEmitter
|
||||
Vector.Mixin()
|
||||
)
|
||||
spy = jasmine.createSpy 'listener'
|
||||
|
||||
it 'can detect changes', ->
|
||||
|
||||
O.on 'xChanged', spy
|
||||
O.on 'yChanged', spy
|
||||
O.on 'vectorChanged', spy
|
||||
|
||||
O.setVector [20, 20]
|
||||
|
||||
expect(spy.callCount).toEqual 3
|
||||
|
||||
it 'can detect changes in the correct order', ->
|
||||
|
||||
accum = 300
|
||||
O.on 'xChanged yChanged', -> accum /= 10
|
||||
O.on 'vectorChanged', -> accum += 200
|
||||
|
||||
O.setVector [20, 20]
|
||||
|
||||
expect(accum).toEqual 203
|
18
packages/math/vertice.coffee
Normal file
18
packages/math/vertice.coffee
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Vertice operations.
|
||||
|
||||
# **Vertice** is a utility class to help with vertice operations. A vertice
|
||||
# is implemented as a 2-element array. Element 0 is *x* and element 1 is *y*.
|
||||
|
||||
# Translate a vertice from an origin point using rotation and scale.
|
||||
export translate = (v, origin, rotation = 0, scale = 1) ->
|
||||
|
||||
difference = [v[0] - origin[0], v[1] - origin[1]]
|
||||
magnitude = scale * Math.sqrt(
|
||||
difference[0] * difference[0] + difference[1] * difference[1]
|
||||
)
|
||||
rotation += Math.atan2 difference[1], difference[0]
|
||||
|
||||
return [
|
||||
origin[0] + Math.cos(rotation) * magnitude
|
||||
origin[1] + Math.sin(rotation) * magnitude
|
||||
]
|
243
packages/mixins/event-emitter.js
Normal file
243
packages/mixins/event-emitter.js
Normal file
|
@ -0,0 +1,243 @@
|
|||
function createListener(fn, that, type, namespace, once) {
|
||||
return {
|
||||
fn, that, type, namespace, once,
|
||||
bound: that ? fn.bind(that) : fn,
|
||||
}
|
||||
}
|
||||
|
||||
export function EventEmitterMixin(Superclass) {
|
||||
|
||||
return class EventEmitter extends Superclass {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.events = {};
|
||||
this.namespaces = {};
|
||||
}
|
||||
|
||||
addListener(...args) { return this.on(...args); }
|
||||
|
||||
// Notify ALL the listeners!
|
||||
emit(...args) {
|
||||
const type = args[0];
|
||||
const listeners = this.lookupEmitListeners(type);
|
||||
if (0 === listeners.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const {once, type, namespace, fn, bound, that} of listeners) {
|
||||
const offset = type !== '*' ? 1 : 0;
|
||||
|
||||
if (once) {
|
||||
this.removeListener(`${type}.${namespace}`, fn);
|
||||
}
|
||||
|
||||
// Fast path...
|
||||
if (args.length === offset) {
|
||||
bound()
|
||||
}
|
||||
|
||||
else if (args.length === offset + 1) {
|
||||
bound(
|
||||
args[offset + 0]
|
||||
);
|
||||
}
|
||||
|
||||
else if (args.length === offset + 2) {
|
||||
bound(
|
||||
args[offset + 0],
|
||||
args[offset + 1]
|
||||
)
|
||||
}
|
||||
|
||||
else if (args.length === offset + 3) {
|
||||
bound(
|
||||
args[offset + 0],
|
||||
args[offset + 1],
|
||||
args[offset + 2]
|
||||
)
|
||||
}
|
||||
|
||||
else if (args.length === offset + 4) {
|
||||
bound(
|
||||
args[offset + 0],
|
||||
args[offset + 1],
|
||||
args[offset + 2],
|
||||
args[offset + 3]
|
||||
)
|
||||
}
|
||||
|
||||
else if (args.length === offset + 5) {
|
||||
bound(
|
||||
args[offset + 0],
|
||||
args[offset + 1],
|
||||
args[offset + 2],
|
||||
args[offset + 3],
|
||||
args[offset + 4]
|
||||
)
|
||||
}
|
||||
|
||||
// Slow path...
|
||||
else {
|
||||
fn.apply(that, args.slice(offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lookupEmitListeners(type) {
|
||||
return ['*', type].reduce((r, type) => {
|
||||
if (type in this.events) {
|
||||
r.push(...this.events[type]);
|
||||
}
|
||||
return r;
|
||||
}, []);
|
||||
}
|
||||
|
||||
off (typesOrType, fn) {
|
||||
parseTypes(typesOrType).forEach((typeOrCompositeType) => {
|
||||
this.offSingleEvent(typeOrCompositeType, fn);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
offSingleEvent(typeOrCompositeType, fn) {
|
||||
const [type, namespace] = decomposeType(typeOrCompositeType);
|
||||
|
||||
// Function.
|
||||
if ('function' === typeof fn) {
|
||||
const lists = [];
|
||||
|
||||
if ((type in this.events)) {
|
||||
lists.push(this.events);
|
||||
}
|
||||
|
||||
if (
|
||||
(namespace in this.namespaces)
|
||||
&& (type in this.namespaces[namespace])
|
||||
) {
|
||||
lists.push(this.namespaces[namespace]);
|
||||
}
|
||||
|
||||
lists.forEach((listeners) => {
|
||||
listeners[type] = listeners[type].filter((listener) => {
|
||||
return listener.fn !== fn;
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Only type.
|
||||
if (0 === namespace.length) {
|
||||
|
||||
if (type in this.events) {
|
||||
delete this.events[type];
|
||||
}
|
||||
|
||||
for (const namespace in this.namespaces) {
|
||||
const namespaceEvents = this.namespaces[namespace];
|
||||
if (type in namespaceEvents) {
|
||||
delete namespaceEvents[type];
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Only namespace.
|
||||
if (!(namespace in this.namespaces)) {
|
||||
return;
|
||||
}
|
||||
if (0 === type.length) {
|
||||
for (const type in this.namespaces[namespace]) {
|
||||
this.removeEventListenersFor(type, namespace);
|
||||
}
|
||||
delete this.namespaces[namespace];
|
||||
}
|
||||
|
||||
// Type & namespace.
|
||||
else if (type in this.namespaces[namespace]) {
|
||||
this.removeEventListenersFor(type, namespace);
|
||||
delete this.namespaces[namespace][type];
|
||||
}
|
||||
}
|
||||
|
||||
on(types, fn, that = undefined) {
|
||||
this.on_PRIVATE(types, fn, that, false);
|
||||
return this;
|
||||
}
|
||||
|
||||
on_PRIVATE(typesOrType, fn, that, once) {
|
||||
parseTypes(typesOrType).forEach((typeOrCompositeType) => {
|
||||
this.onSingleEvent(typeOrCompositeType, fn, that, once);
|
||||
});
|
||||
}
|
||||
|
||||
once(types, fn, that = undefined) {
|
||||
this.on_PRIVATE(types, fn, that, true);
|
||||
return this;
|
||||
}
|
||||
|
||||
onSingleEvent(typeOrCompositeType, fn, that, once) {
|
||||
const [type, namespace] = decomposeType(typeOrCompositeType);
|
||||
const listener = createListener(fn, that, type, namespace, once);
|
||||
|
||||
if (!(type in this.events)) {
|
||||
this.events[type] = [];
|
||||
}
|
||||
this.events[type].push(listener);
|
||||
|
||||
if (!(namespace in this.namespaces)) {
|
||||
this.namespaces[namespace] = {};
|
||||
}
|
||||
if (!(type in this.namespaces[namespace])) {
|
||||
this.namespaces[namespace][type] = [];
|
||||
}
|
||||
this.namespaces[namespace][type].push(listener);
|
||||
}
|
||||
|
||||
removeEventListenersFor(type, namespace) {
|
||||
|
||||
for (const {fn} of this.namespaces[namespace][type]) {
|
||||
this.events[type] = this.events[type].filter((listener) => {
|
||||
return listener.fn !== fn;
|
||||
});
|
||||
}
|
||||
|
||||
if (0 === this.events[type].length) {
|
||||
delete this.events[type];
|
||||
}
|
||||
}
|
||||
|
||||
removeListener(...args) { return this.off(...args); }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export function decomposeType(typeOrCompositeType) {
|
||||
const index = typeOrCompositeType.indexOf('.');
|
||||
const isCompositeType = -1 !== index;
|
||||
if (isCompositeType) {
|
||||
return [
|
||||
typeOrCompositeType.substr(0, index),
|
||||
typeOrCompositeType.substr(index + 1),
|
||||
];
|
||||
}
|
||||
return [typeOrCompositeType, ''];
|
||||
}
|
||||
|
||||
// Split, flatten, and trim.
|
||||
export function parseTypes(typesOrType) {
|
||||
const types = Array.isArray(typesOrType) ? typesOrType : [typesOrType];
|
||||
return types.map((type) => {
|
||||
return type.split(' ');
|
||||
|
||||
}).reduce((r, types) => {
|
||||
r.push(...types);
|
||||
return r;
|
||||
|
||||
}, []).map((type) => {
|
||||
return type.trim();
|
||||
|
||||
});
|
||||
}
|
4
packages/mixins/index.js
Normal file
4
packages/mixins/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
export {EventEmitterMixin as EventEmitter} from './event-emitter';
|
||||
export {LfoMixin as Lfo} from './lfo';
|
||||
export {PropertyMixin as Property} from './property';
|
||||
export {TransitionMixin as Transition} from './transition';
|
9
packages/mixins/lfo/index.js
Normal file
9
packages/mixins/lfo/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import LfoResult from './result';
|
||||
|
||||
export function LfoMixin (Superclass) {
|
||||
return class Lfo extends Superclass {
|
||||
lfo(properties, duration) {
|
||||
return new LfoResult(this, properties, duration);
|
||||
}
|
||||
};
|
||||
}
|
142
packages/mixins/lfo/modulated-property.js
Normal file
142
packages/mixins/lfo/modulated-property.js
Normal file
|
@ -0,0 +1,142 @@
|
|||
import {compose} from '@avocado/core';
|
||||
|
||||
import {EventEmitterMixin as EventEmitter} from '../event-emitter';
|
||||
import {PropertyMixin as Property} from '../property';
|
||||
import {TransitionMixin as Transition} from '../transition';
|
||||
|
||||
const Modulator = {
|
||||
|
||||
Flat() {
|
||||
return (location) => {
|
||||
return .5;
|
||||
}
|
||||
},
|
||||
|
||||
Linear() {
|
||||
return (location) => {
|
||||
return location;
|
||||
}
|
||||
},
|
||||
|
||||
Random({variance = .4} = {}) {
|
||||
return (location) => {
|
||||
return Math.abs((Math.random() * (variance + variance) - variance)) % 1;
|
||||
}
|
||||
},
|
||||
|
||||
Sine() {
|
||||
return (location) => .5 * (1 + Math.sin(location * Math.PI * 2))
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
const decorate = compose(
|
||||
Transition,
|
||||
Property('frequency', {
|
||||
default: 0,
|
||||
}),
|
||||
Property('location', {
|
||||
default: 0,
|
||||
}),
|
||||
Property('magnitude', {
|
||||
default: 0,
|
||||
}),
|
||||
EventEmitter,
|
||||
);
|
||||
|
||||
class ModulatedProperty {
|
||||
|
||||
constructor(
|
||||
object, key,
|
||||
{frequency, location, magnitude, median, modulators}
|
||||
) {
|
||||
|
||||
this.object = object;
|
||||
this.key = key;
|
||||
|
||||
if (!modulators) {
|
||||
modulators = [Modulator.Linear];
|
||||
}
|
||||
if (!Array.isArray(modulators)) {
|
||||
modulators = [modulators];
|
||||
}
|
||||
|
||||
this.median = median;
|
||||
|
||||
this.on('magnitudeChanged', () => {
|
||||
this.magnitude2 = this.magnitude() * 2;
|
||||
});
|
||||
|
||||
this.setFrequency(frequency);
|
||||
this.setLocation(location || 0);
|
||||
this.setMagnitude(magnitude);
|
||||
|
||||
if (this.median) {
|
||||
this.min = this.median - magnitude;
|
||||
}
|
||||
|
||||
const modulatorFunction = (modulator) => {
|
||||
if ('string' === typeof modulator) {
|
||||
return Modulator[modulator] ? Modulator[modulator] : Modulator.Linear;
|
||||
}
|
||||
else if ('function' === typeof modulator) {
|
||||
return modulator;
|
||||
}
|
||||
else {
|
||||
return Modulator.Linear;
|
||||
}
|
||||
};
|
||||
|
||||
this.modulators = modulators.map((modulator) => {
|
||||
if ('object' !== typeof modulator) {
|
||||
return modulatorFunction(modulator)(modulator);
|
||||
}
|
||||
if (modulator.f) {
|
||||
return modulatorFunction(modulator.f)(modulator);
|
||||
}
|
||||
else {
|
||||
[key] = Object.keys(modulator)
|
||||
return modulatorFunction(key)(modulator[key]);
|
||||
}
|
||||
});
|
||||
|
||||
this.transitions = [];
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
|
||||
this.transitions.forEach((transition) => {
|
||||
transition.tick(elapsed);
|
||||
});
|
||||
|
||||
const frequency = this.frequency();
|
||||
let location = this.location();
|
||||
|
||||
location += elapsed;
|
||||
if (location > frequency) {
|
||||
location -= frequency;
|
||||
}
|
||||
|
||||
this.setLocation(location);
|
||||
|
||||
const min = this.median ? this.min : this.object[this.key];
|
||||
|
||||
const value = this.modulators.reduce(
|
||||
(value, m) => value + m(location / frequency),
|
||||
0
|
||||
) / this.modulators.length;
|
||||
|
||||
this.object[this.key] = min + value * this.magnitude2;
|
||||
}
|
||||
|
||||
transition(...args) {
|
||||
const transition = Transition.prototype.transition.apply(this, args);
|
||||
this.transitions.push(transition);
|
||||
transition.promise.then(() => {
|
||||
this.transitions.splice(this.transitions.indexOf(transition), 1);
|
||||
});
|
||||
return transition;
|
||||
}
|
||||
}
|
||||
|
||||
export default decorate(ModulatedProperty);
|
66
packages/mixins/lfo/result.js
Normal file
66
packages/mixins/lfo/result.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
import ModulatedProperty from './modulated-property';
|
||||
|
||||
export default class LfoResult {
|
||||
|
||||
constructor(object, properties, duration = 0) {
|
||||
|
||||
this.duration = duration;
|
||||
|
||||
this.elapsed = 0;
|
||||
this.isRunning = false;
|
||||
this.object = {};
|
||||
this.properties = {};
|
||||
|
||||
this.start();
|
||||
|
||||
this.deferred = {};
|
||||
this.promise = this.deferred.promise = new Promise((resolve, reject) => {
|
||||
this.deferred.resolve = resolve;
|
||||
this.deferred.reject = reject;
|
||||
});
|
||||
|
||||
for (const key in properties) {
|
||||
this.properties[key] = new ModulatedProperty(object, key, {
|
||||
frequency: this.duration,
|
||||
...properties[key]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
property(key) {
|
||||
return this.properties[key];
|
||||
}
|
||||
|
||||
start() {
|
||||
this.elapsed = 0;
|
||||
this.isRunning = true;
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
if (!this.isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
let finished = false;
|
||||
|
||||
if (this.duration > 0 && this.duration <= (this.elapsed += elapsed)) {
|
||||
finished = true;
|
||||
elapsed = this.elapsed - this.duration;
|
||||
this.elapsed = this.duration;
|
||||
}
|
||||
|
||||
for (const key in properties) {
|
||||
properties[key].tick(elapsed);
|
||||
}
|
||||
|
||||
if (finished) {
|
||||
this.deferred.resolve();
|
||||
this.stop();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
7
packages/mixins/package.json
Normal file
7
packages/mixins/package.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@avocado/mixins",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"author": "cha0s",
|
||||
"license": "MIT"
|
||||
}
|
77
packages/mixins/property.js
Normal file
77
packages/mixins/property.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
export function PropertyMixin(key, meta = {}) {
|
||||
|
||||
if (!meta || 'object' !== typeof meta) {
|
||||
throw new TypeError(
|
||||
`Expected meta for Property(${
|
||||
key
|
||||
}) to be object. ${
|
||||
JSON.stringify(meta)
|
||||
} given.`
|
||||
);
|
||||
}
|
||||
|
||||
return (Superclass) => {
|
||||
|
||||
if (Superclass.prototype[key]) {
|
||||
throw new TypeError(`can't redefine Avocado property "${key}"`);
|
||||
}
|
||||
|
||||
meta.getContext = meta.getContext || function() { return this; };
|
||||
if ('identity' === meta.transformProperty) {
|
||||
meta.transformProperty = (key) => key;
|
||||
}
|
||||
else if (!meta.transformProperty) {
|
||||
meta.transformProperty = (key) => `${key}_PRIVATE_PROPERTY`;
|
||||
}
|
||||
|
||||
meta.eq = meta.eq || function (l, r) {
|
||||
return l === r;
|
||||
}
|
||||
|
||||
meta.get = meta.get || function(value) {
|
||||
return this[meta.transformProperty(key)];
|
||||
}
|
||||
|
||||
meta.set = meta.set || function(value) {
|
||||
this[meta.transformProperty(key)] = value;
|
||||
}
|
||||
|
||||
let metaDefault;
|
||||
if (null === meta.default) {
|
||||
metaDefault = null;
|
||||
}
|
||||
else if (undefined === meta.default) {
|
||||
metaDefault = undefined;
|
||||
}
|
||||
else {
|
||||
metaDefault = JSON.parse(JSON.stringify(meta.default));
|
||||
}
|
||||
|
||||
class Property extends Superclass {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
if (undefined !== metaDefault) {
|
||||
meta.set.call(this, metaDefault);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.defineProperty(Property.prototype, key, {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return meta.get.call(this);
|
||||
},
|
||||
set: function (value) {
|
||||
const old = meta.get.call(this);
|
||||
meta.set.call(this, value);
|
||||
if (this.emit && !meta.eq.call(this, old, value)) {
|
||||
this.emit(`${key}Changed`, old, value)
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return Property;
|
||||
}
|
||||
}
|
170
packages/mixins/transition/easing.js
Normal file
170
packages/mixins/transition/easing.js
Normal file
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* jQuery Easing v1.3 - http://gsgd.co.uk/sandbox/jquery/easing/
|
||||
*
|
||||
* Uses the built in easing capabilities added In jQuery 1.1
|
||||
* to offer multiple easing options
|
||||
*
|
||||
* TERMS OF USE - jQuery Easing
|
||||
*
|
||||
* Open source under the BSD License.
|
||||
*
|
||||
* Copyright © 2008 George McGinley Smith
|
||||
* All rights reserved.
|
||||
*
|
||||
* Modified by Ruben Rodriguez <cha0s@therealcha0s.net>
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* Neither the name of the author nor the names of contributors may be used to
|
||||
* endorse or promote products derived from this software without specific
|
||||
* prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
export default {
|
||||
linear: function (t, b, c, d) {
|
||||
return b + c * t/d
|
||||
},
|
||||
easeInQuad: function (t, b, c, d) {
|
||||
return c*(t/=d)*t + b;
|
||||
},
|
||||
easeOutQuad: function (t, b, c, d) {
|
||||
return -c *(t/=d)*(t-2) + b;
|
||||
},
|
||||
easeInOutQuad: function (t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return c/2*t*t + b;
|
||||
return -c/2 * ((--t)*(t-2) - 1) + b;
|
||||
},
|
||||
easeInCubic: function (t, b, c, d) {
|
||||
return c*(t/=d)*t*t + b;
|
||||
},
|
||||
easeOutCubic: function (t, b, c, d) {
|
||||
return c*((t=t/d-1)*t*t + 1) + b;
|
||||
},
|
||||
easeInOutCubic: function (t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return c/2*t*t*t + b;
|
||||
return c/2*((t-=2)*t*t + 2) + b;
|
||||
},
|
||||
easeInQuart: function (t, b, c, d) {
|
||||
return c*(t/=d)*t*t*t + b;
|
||||
},
|
||||
easeOutQuart: function (t, b, c, d) {
|
||||
return -c * ((t=t/d-1)*t*t*t - 1) + b;
|
||||
},
|
||||
easeInOutQuart: function (t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return c/2*t*t*t*t + b;
|
||||
return -c/2 * ((t-=2)*t*t*t - 2) + b;
|
||||
},
|
||||
easeInQuint: function (t, b, c, d) {
|
||||
return c*(t/=d)*t*t*t*t + b;
|
||||
},
|
||||
easeOutQuint: function (t, b, c, d) {
|
||||
return c*((t=t/d-1)*t*t*t*t + 1) + b;
|
||||
},
|
||||
easeInOutQuint: function (t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b;
|
||||
return c/2*((t-=2)*t*t*t*t + 2) + b;
|
||||
},
|
||||
easeInSine: function (t, b, c, d) {
|
||||
return -c * Math.cos(t/d * (Math.PI/2)) + c + b;
|
||||
},
|
||||
easeOutSine: function (t, b, c, d) {
|
||||
return c * Math.sin(t/d * (Math.PI/2)) + b;
|
||||
},
|
||||
easeInOutSine: function (t, b, c, d) {
|
||||
return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b;
|
||||
},
|
||||
easeInExpo: function (t, b, c, d) {
|
||||
return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b;
|
||||
},
|
||||
easeOutExpo: function (t, b, c, d) {
|
||||
return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
|
||||
},
|
||||
easeInOutExpo: function (t, b, c, d) {
|
||||
if (t==0) return b;
|
||||
if (t==d) return b+c;
|
||||
if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b;
|
||||
return c/2 * (-Math.pow(2, -10 * --t) + 2) + b;
|
||||
},
|
||||
easeInCirc: function (t, b, c, d) {
|
||||
return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b;
|
||||
},
|
||||
easeOutCirc: function (t, b, c, d) {
|
||||
return c * Math.sqrt(1 - (t=t/d-1)*t) + b;
|
||||
},
|
||||
easeInOutCirc: function (t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b;
|
||||
return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b;
|
||||
},
|
||||
easeInElastic: function (t, b, c, d) {
|
||||
var s=1.70158;var p=0;var a=c;
|
||||
if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3;
|
||||
if (a < Math.abs(c)) { a=c; var s=p/4; }
|
||||
else var s = p/(2*Math.PI) * Math.asin (c/a);
|
||||
return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
|
||||
},
|
||||
easeOutElastic: function (t, b, c, d) {
|
||||
var s=1.70158;var p=0;var a=c;
|
||||
if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3;
|
||||
if (a < Math.abs(c)) { a=c; var s=p/4; }
|
||||
else var s = p/(2*Math.PI) * Math.asin (c/a);
|
||||
return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b;
|
||||
},
|
||||
easeInOutElastic: function (t, b, c, d) {
|
||||
var s=1.70158;var p=0;var a=c;
|
||||
if (t==0) return b; if ((t/=d/2)==2) return b+c; if (!p) p=d*(.3*1.5);
|
||||
if (a < Math.abs(c)) { a=c; var s=p/4; }
|
||||
else var s = p/(2*Math.PI) * Math.asin (c/a);
|
||||
if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
|
||||
return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b;
|
||||
},
|
||||
easeInBack: function (t, b, c, d, s) {
|
||||
if (s == undefined) s = 1.70158;
|
||||
return c*(t/=d)*t*((s+1)*t - s) + b;
|
||||
},
|
||||
easeOutBack: function (t, b, c, d, s) {
|
||||
if (s == undefined) s = 1.70158;
|
||||
return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
|
||||
},
|
||||
easeInOutBack: function (t, b, c, d, s) {
|
||||
if (s == undefined) s = 1.70158;
|
||||
if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b;
|
||||
return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
|
||||
},
|
||||
easeInBounce: function (t, b, c, d) {
|
||||
return c - easing.easeOutBounce (d-t, 0, c, d) + b;
|
||||
},
|
||||
easeOutBounce: function (t, b, c, d) {
|
||||
if ((t/=d) < (1/2.75)) {
|
||||
return c*(7.5625*t*t) + b;
|
||||
} else if (t < (2/2.75)) {
|
||||
return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b;
|
||||
} else if (t < (2.5/2.75)) {
|
||||
return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b;
|
||||
} else {
|
||||
return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b;
|
||||
}
|
||||
},
|
||||
easeInOutBounce: function (t, b, c, d) {
|
||||
if (t < d/2) return easing.easeInBounce (t*2, 0, c, d) * .5 + b;
|
||||
return easing.easeOutBounce (t*2-d, 0, c, d) * .5 + c*.5 + b;
|
||||
}
|
||||
};
|
38
packages/mixins/transition/index.js
Normal file
38
packages/mixins/transition/index.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
// **Transition** is a **Mixin** which lends the ability to handle timed
|
||||
// transitions of arbitrary property methods residing on the mixed-in object.
|
||||
//
|
||||
// You can use this mixin like this:
|
||||
//
|
||||
// @Transition
|
||||
// class YourClass {
|
||||
// constructor() {
|
||||
// this.x = 0;
|
||||
// }
|
||||
// get x() {
|
||||
// return this.x;
|
||||
// }
|
||||
// set x(x) {
|
||||
// this.x = x;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// const yourObject = new YourClass();
|
||||
// yourObject.transition({x: 100}, 2000, 'easeOutQuad');
|
||||
//
|
||||
// The value of yourObject.x will transition towards 100 over the course of
|
||||
// 2000 milliseconds. ***NOTE:*** yourObject **must** have a getter and a
|
||||
// setter for "x" defined.
|
||||
//
|
||||
// This function was heavily inspired by the existence of
|
||||
// [jQuery.animate](http://api.jquery.com/animate/), though the API is ***NOT***
|
||||
// compatible.
|
||||
|
||||
import TransitionResult from './result';
|
||||
|
||||
export function TransitionMixin(Superclass) {
|
||||
return class Transition extends Superclass {
|
||||
transition(props, duration, easing) {
|
||||
return new TransitionResult(this, props, duration, easing);
|
||||
}
|
||||
};
|
||||
}
|
120
packages/mixins/transition/result.js
Normal file
120
packages/mixins/transition/result.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
import {compose} from '@avocado/core';
|
||||
|
||||
import {EventEmitterMixin as EventEmitter} from '../event-emitter';
|
||||
|
||||
import easingFunctions from './easing';
|
||||
|
||||
const decorate = compose(
|
||||
EventEmitter,
|
||||
);
|
||||
|
||||
class TransitionResult {
|
||||
|
||||
constructor(subject, props, duration, easing) {
|
||||
|
||||
// Speed might not get passed. If it doesn't, default to 100
|
||||
// milliseconds.
|
||||
this.duration = parseInt(duration || 100);
|
||||
this.elapsed = 0;
|
||||
this.isEmittingProgress_PRIVATE = false;
|
||||
this.props = props;
|
||||
this.subject = subject;
|
||||
|
||||
if ('function' === typeof easing) {
|
||||
this.easing = easing;
|
||||
}
|
||||
// If easing isn't passed in as a function, attempt to look it up
|
||||
// as a string key into Transition.easing. If that fails, then
|
||||
// default to 'easeOutQuad'.
|
||||
else {
|
||||
this.easing = easingFunctions[easing] || easingFunctions['easeOutQuad'];
|
||||
}
|
||||
|
||||
this.original = {};
|
||||
this.change = {};
|
||||
for (const i in this.props) {
|
||||
const value = this.subject[i];
|
||||
this.original[i] = value;
|
||||
this.change[i] = this.props[i] - value;
|
||||
}
|
||||
|
||||
// Set up the transition object.
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
this.on('stopped', () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
get isEmittingProgress() {
|
||||
return this.isEmittingProgress_PRIVATE;
|
||||
}
|
||||
|
||||
set isEmittingProgress(isEmittingProgress) {
|
||||
this.isEmittingProgress_PRIVATE = isEmittingProgress;
|
||||
}
|
||||
|
||||
// Immediately finish the transition. This will leave the object
|
||||
// in the fully transitioned state.
|
||||
skipTransition() {
|
||||
|
||||
// Just trick it into thinking the time passed and do one last
|
||||
// tick.
|
||||
this.elapsed = this.duration;
|
||||
this.tick(0);
|
||||
}
|
||||
|
||||
// Immediately stop the transition. This will leave the object in
|
||||
// its current state; potentially partially transitioned.
|
||||
stopTransition() {
|
||||
|
||||
// Let any listeners know that the transition is complete.
|
||||
if (this.isEmittingProgress_PRIVATE) {
|
||||
this.emit('progress', [this.elapsed, this.duration]);
|
||||
}
|
||||
this.emit('stopped');
|
||||
}
|
||||
|
||||
// Tick callback. Called repeatedly while this transition is
|
||||
// running.
|
||||
tick(elapsed) {
|
||||
|
||||
// Update the transition's elapsed time.
|
||||
this.elapsed += elapsed;
|
||||
|
||||
// If we've overshot the duration, we'll fix it up here, so
|
||||
// things never transition too far (through the end point).
|
||||
if (this.elapsed >= this.duration) {
|
||||
this.elapsed = this.duration;
|
||||
for (const i in this.change) {
|
||||
if (this.change[i]) {
|
||||
this.subject[i] = this.props[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
// Do easing for each property that actually changed.
|
||||
for (const i in this.change) {
|
||||
if (this.change[i]) {
|
||||
this.subject[i] = this.easing(
|
||||
this.elapsed,
|
||||
this.original[i],
|
||||
this.change[i],
|
||||
this.duration
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop if we're done.
|
||||
if (this.elapsed === this.duration) {
|
||||
this.stopTransition();
|
||||
}
|
||||
else {
|
||||
if (this.isEmittingProgress_PRIVATE) {
|
||||
this.emit('progress', [this.elapsed, this.duration]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default decorate(TransitionResult);
|
51
packages/react/animation.coffee
Normal file
51
packages/react/animation.coffee
Normal file
|
@ -0,0 +1,51 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {AnimationView, Ticker} from '@avocado/timing'
|
||||
class AnimationComponent extends React.Component
|
||||
|
||||
@defaultProps:
|
||||
|
||||
animation: null
|
||||
image: null
|
||||
|
||||
isTicking: true
|
||||
|
||||
constructor: (props) ->
|
||||
|
||||
super props
|
||||
|
||||
@animationView = new AnimationView()
|
||||
@animationView.on [
|
||||
'animationChanged'
|
||||
'imageChanged'
|
||||
'sourceRectangleChanged'
|
||||
], @tickContainer
|
||||
|
||||
@ticker = new Ticker.OutOfBand()
|
||||
@ticker.on 'tick', (elapsed) => @props.animation?.tick elapsed
|
||||
|
||||
componentWillUnmount: ->
|
||||
|
||||
@animationView.off [
|
||||
'animationChanged'
|
||||
'imageChanged'
|
||||
'sourceRectangleChanged'
|
||||
], @tickContainer
|
||||
|
||||
@ticker.off 'tick'
|
||||
@ticker.stop()
|
||||
|
||||
render: ->
|
||||
|
||||
# Side-effects.
|
||||
@animationView.setAnimation @props.animation
|
||||
@animationView.setImage @props.image
|
||||
|
||||
@props.setIntoContainer @animationView
|
||||
|
||||
return null
|
||||
|
||||
tickContainer: => @props.tickContainer()
|
||||
|
||||
export default AnimationComponent
|
79
packages/react/container.coffee
Normal file
79
packages/react/container.coffee
Normal file
|
@ -0,0 +1,79 @@
|
|||
import shallowequal from 'shallowequal'
|
||||
import * as React from 'react'
|
||||
|
||||
import {Container, Renderer} from '@avocado/graphics'
|
||||
import {Vector} from '@avocado/math'
|
||||
|
||||
class ContainerComponent extends React.Component
|
||||
|
||||
constructor: (props) ->
|
||||
|
||||
super()
|
||||
|
||||
@isDirty = true
|
||||
@renderer = new Renderer props.size, 'canvas'
|
||||
@container = new Container()
|
||||
|
||||
@defaultProps: size: [0, 0]
|
||||
|
||||
componentDidMount: ->
|
||||
|
||||
@containerRef.appendChild @renderer.element()
|
||||
|
||||
window.addEventListener 'resize', @recalculateBackgroundSize
|
||||
|
||||
componentDidUpdate: ->
|
||||
|
||||
# Performance: Check if the children actually changed after render.
|
||||
@isDirty = shallowequal @previousChildren, @container.children()
|
||||
|
||||
# Tick the first time.
|
||||
return unless @isDirty
|
||||
|
||||
@tick()
|
||||
@isDirty = false
|
||||
|
||||
componentWillUnmount: ->
|
||||
|
||||
window.removeEventListener 'resize', @recalculateBackgroundSize
|
||||
|
||||
@renderer.destroy(); @renderer = null
|
||||
|
||||
recalculateBackgroundSize: =>
|
||||
|
||||
s = getComputedStyle @renderer.element()
|
||||
domSize = [s.width, s.height].map (n) -> parseInt n
|
||||
|
||||
[w, h] = Vector.mul [16, 16], Vector.div domSize, @props.size
|
||||
@renderer.element().style.backgroundSize = "#{w}px #{h}px"
|
||||
|
||||
render: ->
|
||||
|
||||
# Literally one big side-effect.
|
||||
@renderer.resize @props.size
|
||||
@recalculateBackgroundSize()
|
||||
|
||||
@previousChildren = @container.children()
|
||||
@container.removeChildren()
|
||||
children = React.Children.map @props.children, (child) =>
|
||||
|
||||
React.cloneElement child,
|
||||
|
||||
setIntoContainer: (renderable) =>
|
||||
|
||||
return unless renderable
|
||||
@container.addChild renderable
|
||||
|
||||
tickContainer: @tick
|
||||
|
||||
<div
|
||||
className="container"
|
||||
ref={@setContainerRef}
|
||||
style={'lineHeight': 0}
|
||||
>{children}</div>
|
||||
|
||||
setContainerRef: (@containerRef) => return
|
||||
|
||||
tick: => @renderer.render @container
|
||||
|
||||
export default ContainerComponent
|
21
packages/react/image.coffee
Normal file
21
packages/react/image.coffee
Normal file
|
@ -0,0 +1,21 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {all, call, put, takeEvery} from 'redux-saga/effects'
|
||||
|
||||
import {Image, Sprite} from '@avocado/graphics'
|
||||
|
||||
import {processActions} from './util'
|
||||
|
||||
class ImageComponent extends React.Component
|
||||
|
||||
@defaultProps: image: null
|
||||
|
||||
image: -> @props.image
|
||||
|
||||
render: ->
|
||||
|
||||
@props.setIntoSprite? @props.image
|
||||
return null
|
||||
|
||||
export default ImageComponent
|
8
packages/react/index.js
Normal file
8
packages/react/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
|
||||
// import Animation from './animation'; export {Animation};
|
||||
// import Container from './container'; export {Container};
|
||||
// import Image from './image'; export {Image};
|
||||
// import Primitives from './primitives'; export {Primitives};
|
||||
// import Room from './room'; export {Room};
|
||||
// import Sprite from './sprite'; export {Sprite};
|
||||
import Vector from './vector'; export {Vector};
|
24
packages/react/layer.coffee
Normal file
24
packages/react/layer.coffee
Normal file
|
@ -0,0 +1,24 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
import {TileLayer2D, TileLayer2DView, Tileset} from '@truss/environment'
|
||||
|
||||
class Layer2D extends React.Component
|
||||
|
||||
@propTypes =
|
||||
|
||||
layer: PropTypes.instanceOf TileLayer2D
|
||||
tileset: PropTypes.instanceOf Tileset
|
||||
|
||||
constructor: (props) ->
|
||||
|
||||
super props
|
||||
@layerView = new TileLayer2DView()
|
||||
|
||||
render: ->
|
||||
|
||||
@layerView.setLayer @props.layer
|
||||
@layerView.setTileset @props.tileset
|
||||
@props.setIntoContainer @layerView
|
||||
|
||||
export default Layer2D
|
13
packages/react/package.json
Normal file
13
packages/react/package.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "@avocado/react",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"author": "cha0s",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"contempo": "1.x",
|
||||
"ponere": "1.x",
|
||||
"react": "^16.5.0",
|
||||
"redux-form": "^7.4.2"
|
||||
}
|
||||
}
|
32
packages/react/primitives.coffee
Normal file
32
packages/react/primitives.coffee
Normal file
|
@ -0,0 +1,32 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import * as React from 'react'
|
||||
|
||||
import {color, Primitives} from '@avocado/graphics'
|
||||
import {Rectangle} from '@avocado/math'
|
||||
|
||||
class PrimitivesComponent extends React.Component
|
||||
|
||||
constructor: (props) ->
|
||||
|
||||
super props
|
||||
|
||||
@primitives = new Primitives()
|
||||
|
||||
@defaultProps:
|
||||
|
||||
lineStyle: Primitives.LineStyle color 0, 0, 0
|
||||
rectangle: [0, 0, 0, 0]
|
||||
|
||||
componentWillUnmount: -> @primitives.destroy()
|
||||
|
||||
render: ->
|
||||
|
||||
@primitives.clear()
|
||||
|
||||
unless Rectangle.isNull @props.rectangle
|
||||
@primitives.drawRectangle @props.rectangle, @props.lineStyle
|
||||
|
||||
@props.setIntoContainer @primitives
|
||||
return null
|
||||
|
||||
export default PrimitivesComponent
|
29
packages/react/room.coffee
Normal file
29
packages/react/room.coffee
Normal file
|
@ -0,0 +1,29 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
import {Room2DView} from '@avocado/environment'
|
||||
|
||||
class Room extends React.Component
|
||||
|
||||
@propTypes =
|
||||
|
||||
roomView: PropTypes.instanceOf(Room2DView).isRequired
|
||||
|
||||
constructor: (props) ->
|
||||
|
||||
super props
|
||||
|
||||
props.roomView.on [
|
||||
'roomChanged', 'tilesetChanged'
|
||||
], @tickContainer
|
||||
|
||||
render: ->
|
||||
|
||||
@props.setIntoContainer @props.roomView
|
||||
@tickContainer()
|
||||
|
||||
return null
|
||||
|
||||
tickContainer: => @props.tickContainer()
|
||||
|
||||
export default Room
|
38
packages/react/sprite.coffee
Normal file
38
packages/react/sprite.coffee
Normal file
|
@ -0,0 +1,38 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import * as React from 'react'
|
||||
|
||||
import {Sprite} from '@avocado/graphics'
|
||||
|
||||
import Image from './image'
|
||||
|
||||
class SpriteComponent extends React.Component
|
||||
|
||||
constructor: (props) ->
|
||||
|
||||
super props
|
||||
|
||||
@sprite = new Sprite()
|
||||
@sprite.on 'imageOrCanvasChanged sourceRectangleChanged', =>
|
||||
@props.tickContainer()
|
||||
|
||||
@defaultProps: image: null
|
||||
|
||||
@propTypes:
|
||||
|
||||
children: PropTypes.element
|
||||
|
||||
componentWillUnmount: -> @sprite.destroy()
|
||||
|
||||
render: ->
|
||||
|
||||
image = React.Children.only @props.children
|
||||
|
||||
child = React.cloneElement image,
|
||||
|
||||
setIntoSprite: (image) => @sprite.setImageOrCanvas image
|
||||
|
||||
@props.setIntoContainer @sprite
|
||||
|
||||
return child
|
||||
|
||||
export default SpriteComponent
|
46
packages/react/vector.js
Normal file
46
packages/react/vector.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
import {Field} from 'redux-form/immutable';
|
||||
|
||||
import contempo from 'contempo';
|
||||
|
||||
import {NumberField} from 'ponere';
|
||||
|
||||
@contempo(require('./vector.scss'))
|
||||
export default class Vector extends React.Component {
|
||||
|
||||
static defaultProps = {
|
||||
max: [99999, 99999],
|
||||
min: [0, 0],
|
||||
xLabel: '',
|
||||
yLabel: '',
|
||||
separatorLabel: 'x',
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="vector control">
|
||||
|
||||
{this.props.label && <label>{this.props.label}</label>}
|
||||
<div className="controls">
|
||||
<div className="control">
|
||||
<Field
|
||||
component={NumberField}
|
||||
name={`${this.props.input.name}[0]`}
|
||||
max={this.props.max[0]}
|
||||
min={this.props.min[0]}
|
||||
></Field>
|
||||
<p className="aside">{this.props.xLabel}</p>
|
||||
</div>
|
||||
<div className="separator">{this.props.separatorLabel}</div>
|
||||
<div className="control">
|
||||
<Field
|
||||
component={NumberField}
|
||||
name={`${this.props.input.name}[1]`}
|
||||
max={this.props.max[1]}
|
||||
min={this.props.min[1]}
|
||||
></Field>
|
||||
<p className="aside">{this.props.yLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
40
packages/react/vector.scss
Normal file
40
packages/react/vector.scss
Normal file
|
@ -0,0 +1,40 @@
|
|||
.vector {
|
||||
display: block;
|
||||
@media (min-width: 1024px) {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.control {
|
||||
float: left;
|
||||
|
||||
> label {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.control label {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
float: right;
|
||||
position: relative;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
float: none;
|
||||
}
|
||||
|
||||
&:after {
|
||||
display: table;
|
||||
content: ' ';
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
float: left;
|
||||
font-size: 0.8em;
|
||||
position: relative;
|
||||
top: 0.4em;
|
||||
}
|
57
packages/resource/index.js
Normal file
57
packages/resource/index.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
// import axios from 'axios';
|
||||
import uuid from 'uuid/v4';
|
||||
|
||||
export class Resource {
|
||||
|
||||
constructor() {
|
||||
this.uri_PRIVATE = undefined;
|
||||
this.uuid_PRIVATE = undefined;
|
||||
this.instanceUuid_PRIVATE = uuid();
|
||||
}
|
||||
|
||||
fromJSON({uri, uuid}) {
|
||||
this.uri_PRIVATE = uri;
|
||||
this.uuid_PRIVATE = uuid;
|
||||
return this;
|
||||
}
|
||||
|
||||
get instanceUuid() {
|
||||
return this.instanceUuid_PRIVATE;
|
||||
}
|
||||
|
||||
regenerateUuid() {
|
||||
this.uuid_PRIVATE = uuid();
|
||||
}
|
||||
|
||||
get uuid() {
|
||||
return this.uuid_PRIVATE;
|
||||
}
|
||||
|
||||
set uuid(uuid) {
|
||||
this.uuid_PRIVATE = uuid;
|
||||
}
|
||||
|
||||
get uri() {
|
||||
return this.uri_PRIVATE;
|
||||
}
|
||||
|
||||
set uri(uri) {
|
||||
this.uri_PRIVATE = uri;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
uuid: this.uuid_PRIVATE,
|
||||
uri: this.uri_PRIVATE,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Resource.createLoader = function(C) {
|
||||
return (uri) => Resource.read(uri).then((O) => (new C()).fromJSON(O));
|
||||
}
|
||||
|
||||
Resource.read = function(uri) {
|
||||
return axios.get(uri).then(response => response.data);
|
||||
}
|
11
packages/resource/package.json
Normal file
11
packages/resource/package.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "@avocado/resource",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"author": "cha0s",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"uuid": "^3.3.2"
|
||||
}
|
||||
}
|
0
packages/server/index.js
Normal file
0
packages/server/index.js
Normal file
10
packages/server/package.json
Normal file
10
packages/server/package.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "@avocado/server",
|
||||
"version": "1.0.2",
|
||||
"main": "index.js",
|
||||
"author": "cha0s",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"socket.io": "2.2.0"
|
||||
}
|
||||
}
|
21
packages/server/socket.js
Normal file
21
packages/server/socket.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
const {EventEmitter} = require('events');
|
||||
const SocketServer = require('socket.io');
|
||||
|
||||
export class Server extends EventEmitter {
|
||||
|
||||
constructor(httpServer) {
|
||||
super();
|
||||
this.io = new SocketServer(httpServer, {
|
||||
path: '/avocado',
|
||||
serveClient: false,
|
||||
});
|
||||
this.io.on('connect', (socket) => {
|
||||
this.emit('connect', socket);
|
||||
});
|
||||
}
|
||||
|
||||
broadcast(message) {
|
||||
this.io.send(message);
|
||||
}
|
||||
|
||||
}
|
71
packages/state/index.js
Normal file
71
packages/state/index.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
import * as I from 'immutable';
|
||||
import immutablediff from 'immutablediff';
|
||||
|
||||
export class StateSynchronizer {
|
||||
|
||||
constructor(statefuls) {
|
||||
this._state = I.Map();
|
||||
this._statefuls = statefuls;
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
acceptStateChange(change) {
|
||||
for (const key in change) {
|
||||
const stateful = this._statefuls[key];
|
||||
if (!stateful) {
|
||||
continue;
|
||||
}
|
||||
stateful.acceptStateChange(change[key]);
|
||||
}
|
||||
}
|
||||
|
||||
diff() {
|
||||
const state = this.state();
|
||||
if (this.previousState === state) {
|
||||
return StateSynchronizer.noChange;
|
||||
}
|
||||
// Take a pure JS diff.
|
||||
const steps = immutablediff(this.previousState, state).toJS();
|
||||
let diff = {};
|
||||
for (const {op, path, value} of steps) {
|
||||
if ('replace' === op || 'add' === op) {
|
||||
if ('/' === path) {
|
||||
diff = value;
|
||||
}
|
||||
else {
|
||||
const parts = path.split('/');
|
||||
parts.shift();
|
||||
let walk = diff;
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
const part = parts[i];
|
||||
walk[part] = walk[part] || {};
|
||||
if (i === parts.length - 1) {
|
||||
walk[part] = value;
|
||||
}
|
||||
else {
|
||||
walk = walk[part];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Side-effect.
|
||||
this.previousState = this.state();
|
||||
return diff;
|
||||
}
|
||||
|
||||
state() {
|
||||
this.updateState();
|
||||
return this._state;
|
||||
}
|
||||
|
||||
updateState() {
|
||||
for (const key in this._statefuls) {
|
||||
const stateful = this._statefuls[key];
|
||||
this._state = this._state.set(key, stateful.state());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StateSynchronizer.noChange = {};
|
7
packages/state/package.json
Normal file
7
packages/state/package.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@avocado/state",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"author": "cha0s",
|
||||
"license": "MIT"
|
||||
}
|
54
packages/timing/animation-frame.coffee
Normal file
54
packages/timing/animation-frame.coffee
Normal file
|
@ -0,0 +1,54 @@
|
|||
|
||||
# Adapted from https://gist.github.com/paulirish/1579671#gistcomment-91515
|
||||
raf = window.requestAnimationFrame
|
||||
caf = window.cancelAnimationFrame
|
||||
|
||||
w = window
|
||||
for vendor in ['ms', 'moz', 'webkit', 'o']
|
||||
break if raf
|
||||
raf = w["#{vendor}RequestAnimationFrame"]
|
||||
caf = (
|
||||
w["#{vendor}CancelAnimationFrame"] or
|
||||
w["#{vendor}CancelRequestAnimationFrame"]
|
||||
)
|
||||
|
||||
# rAF is built in but cAF is not.
|
||||
if raf and not caf
|
||||
browserRaf = raf
|
||||
canceled = {}
|
||||
|
||||
raf = (fn) -> id = browserRaf (time) ->
|
||||
return fn time unless id of canceled
|
||||
delete canceled[id]
|
||||
|
||||
caf = (id) -> canceled[id] = true
|
||||
|
||||
# Handle legacy browsers which don’t implement rAF
|
||||
unless raf
|
||||
targetTime = 0
|
||||
|
||||
raf = (fn) ->
|
||||
targetTime = Math.max targetTime + 16, currentTime = +new Date
|
||||
w.setTimeout (-> fn +new Date), targetTime - currentTime
|
||||
|
||||
caf = (id) -> clearTimeout id
|
||||
|
||||
export requestAnimationFrame = raf
|
||||
export cancelAnimationFrame = caf
|
||||
|
||||
# setInterval, but for animations. :)
|
||||
said = 1
|
||||
handles = {}
|
||||
export setAnimation = (fn) ->
|
||||
id = said++
|
||||
|
||||
handles[id] = raf ifn = do (id) -> (time) ->
|
||||
return unless handles[id]?
|
||||
fn time
|
||||
handles[id] = raf ifn
|
||||
|
||||
return id
|
||||
|
||||
export clearAnimation = (id) ->
|
||||
caf handles[id]
|
||||
delete handles[id]
|
82
packages/timing/animation-view.coffee
Normal file
82
packages/timing/animation-view.coffee
Normal file
|
@ -0,0 +1,82 @@
|
|||
|
||||
import Promise from 'bluebird'
|
||||
|
||||
import {
|
||||
EventEmitter, juggleEvents, Mixin, Property
|
||||
} from '@avocado/composition'
|
||||
import {Container, Image, Sprite} from '@avocado/graphics'
|
||||
import {Vector} from '@avocado/math'
|
||||
|
||||
import {Animation} from './animation'
|
||||
|
||||
export class AnimationView extends Mixin(Container).with(
|
||||
EventEmitter
|
||||
Property 'animation'
|
||||
Property 'image', default: null
|
||||
)
|
||||
|
||||
constructor: (animation) ->
|
||||
|
||||
super()
|
||||
|
||||
@_sprite = null
|
||||
|
||||
@on 'animationChanged', (oldAnimation) => @onAnimationChanged oldAnimation
|
||||
|
||||
@setAnimation animation
|
||||
|
||||
destroy: -> @_sprite?.destroy
|
||||
|
||||
onAnimationChanged: (oldAnimation) ->
|
||||
|
||||
animation = @animation()
|
||||
|
||||
juggleEvents(
|
||||
this, oldAnimation, animation
|
||||
positionChanged: @onAnimationPositionChanged
|
||||
sourceRectangleChanged: @onSourceRectangleChanged
|
||||
)
|
||||
|
||||
juggleEvents(
|
||||
this, oldAnimation, animation
|
||||
imageUriChanged: @onAnimationImageUriChanged
|
||||
)
|
||||
|
||||
@onAnimationImageUriChanged()
|
||||
|
||||
onAnimationImageUriChanged: ->
|
||||
|
||||
return unless animation = @animation()
|
||||
return unless uri = animation.imageUri()
|
||||
|
||||
Image.load(uri).done (image) => @setImage image
|
||||
|
||||
onAnimationPositionChanged: -> @_sprite.setPosition @position()
|
||||
|
||||
onSourceRectangleChanged: =>
|
||||
@_sprite?.setSourceRectangle @animation().sourceRectangle()
|
||||
|
||||
onSpriteSourceRectangleChanged: => @emit 'sourceRectangleChanged'
|
||||
|
||||
setImage: (
|
||||
image
|
||||
) ->
|
||||
|
||||
if @_sprite
|
||||
|
||||
@removeChild @_sprite
|
||||
@_sprite?.off 'sourceRectangleChanged', @onSpriteSourceRectangleChanged
|
||||
@_sprite.destroy()
|
||||
|
||||
return unless image
|
||||
|
||||
# Add to render list before actually setting inner image. This way,
|
||||
# AnimationView::onSetImage is useful for render timing.
|
||||
ownedImage = image.clone()
|
||||
@addChild @_sprite = new Sprite ownedImage
|
||||
@_sprite.on 'sourceRectangleChanged', @onSpriteSourceRectangleChanged
|
||||
@_sprite.setSourceRectangle @animation().sourceRectangle()
|
||||
|
||||
super ownedImage
|
||||
|
||||
sprite: -> @_sprite
|
114
packages/timing/animation.coffee
Normal file
114
packages/timing/animation.coffee
Normal file
|
@ -0,0 +1,114 @@
|
|||
|
||||
import Promise from 'bluebird'
|
||||
|
||||
import {
|
||||
EventEmitter, Mixin, Property
|
||||
} from '@avocado/composition'
|
||||
import {Rectangle, Vector} from '@avocado/math'
|
||||
import {Resource} from '@avocado/resource'
|
||||
import {setterName} from '@avocado/string'
|
||||
import {
|
||||
TimedIndexMixin as TimedIndex
|
||||
} from './timed-index'
|
||||
|
||||
export class Animation extends Mixin(Resource).with(
|
||||
|
||||
EventEmitter
|
||||
|
||||
Property 'direction', default: 0
|
||||
Property 'directionCount', default: 1
|
||||
TimedIndex 'frame'
|
||||
Property 'frameSize', default: [0, 0]
|
||||
Property 'imageUri', default: ''
|
||||
Vector.Mixin(
|
||||
'position', 'x', 'y'
|
||||
x: default: 0
|
||||
y: default: 0
|
||||
)
|
||||
Property 'sourceRectangle', default: [0, 0, 0, 0]
|
||||
Property 'uri', default: ''
|
||||
)
|
||||
|
||||
@load: @createLoad Animation
|
||||
|
||||
@reduce: (O) ->
|
||||
|
||||
O = Object.assign (new Animation()).toJSON(), O
|
||||
|
||||
O.directionCount = parseInt O.directionCount
|
||||
O.direction = parseInt O.direction ? 0
|
||||
O.frameCount = parseInt O.frameCount
|
||||
O.frameRate = parseInt O.frameRate
|
||||
O.frameSize = O.frameSize.map (x) -> parseInt x
|
||||
|
||||
O.uri or= ''
|
||||
O.imageUri or= O.uri.replace '.animation.json', '.png'
|
||||
|
||||
return O
|
||||
|
||||
constructor: ->
|
||||
|
||||
super()
|
||||
|
||||
@on [
|
||||
'directionChanged', 'frameSizeChanged', 'indexChanged'
|
||||
], => @setSourceRectangle @rawSourceRectangle @index()
|
||||
|
||||
@on 'directionCountChanged', => @setDirection @direction()
|
||||
|
||||
clampDirection: (direction) ->
|
||||
|
||||
return 0 if @directionCount() is 1
|
||||
|
||||
direction = Math.min 7, Math.max direction, 0
|
||||
direction = {
|
||||
4: 1
|
||||
5: 1
|
||||
6: 3
|
||||
7: 3
|
||||
}[direction] if @directionCount() is 4 and direction > 3
|
||||
|
||||
direction
|
||||
|
||||
# Only mutates if not Vector.equals size, [0, 0]
|
||||
deriveFrameSize: (size) ->
|
||||
|
||||
return frameSize unless Vector.isZero frameSize = @frameSize() ? [0, 0]
|
||||
return [0, 0] if Vector.isZero size
|
||||
return [0, 0] if @directionCount() is 0
|
||||
return [0, 0] if @frameCount() is 0
|
||||
|
||||
# If the frame size isn't explicitly given, then calculate the
|
||||
# size of one frame using the total number of frames and the total
|
||||
# spritesheet size. Width is calculated by dividing the total
|
||||
# spritesheet width by the number of frames, and the height is the
|
||||
# height of the spritesheet divided by the number of directions
|
||||
# in the animation.
|
||||
return Vector.div size, [@frameCount(), @directionCount()]
|
||||
|
||||
rawSourceRectangle: (index) ->
|
||||
|
||||
return [0, 0, 0, 0] unless frameCount = @frameCount()
|
||||
|
||||
frameSize = @frameSize()
|
||||
Rectangle.compose(
|
||||
Vector.mul frameSize, [
|
||||
(index ? @index()) % frameCount
|
||||
@direction()
|
||||
]
|
||||
frameSize
|
||||
)
|
||||
|
||||
setDirection: (direction) -> super @clampDirection parseInt direction
|
||||
|
||||
setDirectionCount: (directionCount) -> super parseInt directionCount
|
||||
|
||||
toJSON: ->
|
||||
|
||||
defaultImageUri = @uri().replace '.animation.json', '.png'
|
||||
|
||||
directionCount: @directionCount()
|
||||
frameRate: @frameRate()
|
||||
frameCount: @frameCount()
|
||||
frameSize: @frameSize()
|
||||
imageUri: @imageUri() if @imageUri() isnt defaultImageUri
|
36
packages/timing/cps.coffee
Normal file
36
packages/timing/cps.coffee
Normal file
|
@ -0,0 +1,36 @@
|
|||
# **Cps** is used to measure the cycles per second of a process. Avocado uses
|
||||
# this class to measure the cycles per second and renders per second of the
|
||||
# engine itself. If you instantiate **Cps** and call **Cps**::tick()
|
||||
# every time a process runs, you can call **Cps**::count() to found how
|
||||
# many times the cycle runs per second.
|
||||
#
|
||||
# *NOTE:* When you instantiate **Cps**, a **frequency** is specified. You
|
||||
# must call **Cps**.tick() for at least **frequency** milliseconds to get
|
||||
# an accurate reading. Until then, you will read 0.
|
||||
|
||||
export class Cps
|
||||
|
||||
# Instantiate the CPS counter. By default, it counts the cycles every 250
|
||||
# milliseconds.
|
||||
constructor: (frequency = 250) ->
|
||||
|
||||
previous = Date.now()
|
||||
|
||||
setInterval =>
|
||||
|
||||
now = Date.now()
|
||||
elapsed = now - previous
|
||||
previous = now
|
||||
@fps = @c * (1000 / elapsed)
|
||||
@c = 0
|
||||
|
||||
, frequency
|
||||
|
||||
@fps = 0
|
||||
@c = 0
|
||||
|
||||
# Call every time the process you want to measure runs.
|
||||
tick: -> @c++
|
||||
|
||||
# Call to retrieve how many cycles the process runs per second.
|
||||
count: -> @fps
|
13
packages/timing/index.coffee
Normal file
13
packages/timing/index.coffee
Normal file
|
@ -0,0 +1,13 @@
|
|||
|
||||
export {
|
||||
cancelAnimationFrame, clearAnimation, requestAnimationFrame, setAnimation
|
||||
} from './animation-frame'
|
||||
|
||||
export {Animation} from './animation'
|
||||
export {AnimationView} from './animation-view'
|
||||
|
||||
export {Cps} from './cps'
|
||||
|
||||
export {Ticker} from './ticker'
|
||||
|
||||
export {TimedIndexMixin as TimedIndex} from './timed-index'
|
7
packages/timing/package.json
Normal file
7
packages/timing/package.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@avocado/timing",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"author": "cha0s",
|
||||
"license": "MIT"
|
||||
}
|
76
packages/timing/ticker.coffee
Normal file
76
packages/timing/ticker.coffee
Normal file
|
@ -0,0 +1,76 @@
|
|||
import {EventEmitter, MixinOf, Property} from '@avocado/composition'
|
||||
|
||||
export class Ticker extends MixinOf(
|
||||
EventEmitter
|
||||
Property 'frequency', default: 0
|
||||
)
|
||||
|
||||
constructor: (frequency = 0) ->
|
||||
|
||||
super()
|
||||
|
||||
@_remainder = 0
|
||||
@setFrequency frequency
|
||||
|
||||
remaining: -> 1 - @_remainder / @frequency()
|
||||
|
||||
reset: -> @_remainder = 0
|
||||
|
||||
tick: (elapsed) ->
|
||||
|
||||
return if (frequency = @frequency()) is 0
|
||||
|
||||
ticks = 0
|
||||
|
||||
@_remainder += elapsed
|
||||
if @_remainder >= frequency
|
||||
ticks = Math.floor @_remainder / frequency
|
||||
@_remainder -= ticks * frequency
|
||||
|
||||
@emit 'tick', frequency for i in [0...ticks]
|
||||
|
||||
return
|
||||
|
||||
class Ticker.OutOfBand extends Ticker
|
||||
|
||||
constructor: ->
|
||||
|
||||
super()
|
||||
|
||||
@_last = Date.now()
|
||||
@_isStarted = true
|
||||
|
||||
@start()
|
||||
|
||||
elapsedSinceLast: ->
|
||||
|
||||
now = Date.now()
|
||||
elapsed = now - @_last
|
||||
@_last = now
|
||||
|
||||
return elapsed
|
||||
|
||||
reset: ->
|
||||
|
||||
super()
|
||||
|
||||
@_last = Date.now()
|
||||
|
||||
start: ->
|
||||
|
||||
return if @_handle
|
||||
|
||||
_tick = =>
|
||||
|
||||
elapsed = @elapsedSinceLast()
|
||||
@emit 'tick', elapsed
|
||||
setTimeout _tick, 10 - (elapsed - 10)
|
||||
|
||||
@_handle = setTimeout _tick, 10
|
||||
|
||||
stop: ->
|
||||
|
||||
return unless @_handle
|
||||
|
||||
clearTimeout @_handle
|
||||
@_handle = null
|
48
packages/timing/timed-index.coffee
Normal file
48
packages/timing/timed-index.coffee
Normal file
|
@ -0,0 +1,48 @@
|
|||
import {Ticker} from './ticker'
|
||||
|
||||
import {EventEmitter, Mixin, Property} from '@avocado/composition'
|
||||
|
||||
export TimedIndexMixin = (indexName = 'index') -> (Superclass) ->
|
||||
|
||||
_indexCount = "#{indexName}Count"
|
||||
_indexRate = "#{indexName}Rate"
|
||||
|
||||
class TimedIndex extends Mixin(Superclass).with(
|
||||
EventEmitter
|
||||
Property 'index', default: 0
|
||||
Property 'isTicking', default: false
|
||||
Property _indexCount, default: 0
|
||||
Property _indexRate, default: 100
|
||||
)
|
||||
|
||||
constructor: ->
|
||||
|
||||
super()
|
||||
|
||||
@_ticker = new Ticker()
|
||||
@_ticker.setFrequency @[_indexRate]()
|
||||
@_ticker.on 'tick', => @_tick()
|
||||
|
||||
@on "#{_indexRate}Changed", => @_ticker.setFrequency @[_indexRate]()
|
||||
|
||||
_clampIndex: (index) ->
|
||||
|
||||
indexCount = @[_indexCount]()
|
||||
return if indexCount is 0 then 0 else index % @[_indexCount]()
|
||||
|
||||
_tick: ->
|
||||
|
||||
index = @index() + 1
|
||||
@emit 'rollingOver' if index >= @[_indexCount]()
|
||||
@setIndex index
|
||||
|
||||
setIndex: (index, reset = true) ->
|
||||
|
||||
super @_clampIndex index
|
||||
@_ticker.reset() if reset
|
||||
|
||||
start: -> @setIsTicking true
|
||||
|
||||
stop: -> @setIsTicking false
|
||||
|
||||
tick: (elapsed) -> @_ticker.tick elapsed if @isTicking()
|
3
webpack.test.config.js
Normal file
3
webpack.test.config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
mode: 'development',
|
||||
};
|
Loading…
Reference in New Issue
Block a user