chore: initial

This commit is contained in:
cha0s 2019-03-17 23:45:48 -05:00
commit 22fe6261b8
94 changed files with 4844 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules

3
TODO.md Normal file
View File

@ -0,0 +1,3 @@
# TODO
- ❌ remove dependency on decorators

15
package.json Normal file
View 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'"
}
}

View 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',
},
},
});

View 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;
}

View 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',
});

View 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);
}

View 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';

View File

@ -0,0 +1,14 @@
import {Traversal} from './traversal';
export class Action extends Traversal {
static type() {
return 'action';
}
toJSON() {
return {
...super.toJSON(),
type: 'action',
};
}
}

View 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');
}
}
}

View File

View 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()),
};
}
};
}

View 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()),
};
}
}

View 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;
});
});
});
});

View 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;
}
}

View 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);

View 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,
}
}
}

View 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);
});
});
});

View 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);
}

View 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(),
}
}
}

View 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,
};
}
}

View 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(),
};
}
}

View 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()),
};
}
}),
};
}
}

View 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;
}
}

View 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
View File

@ -0,0 +1 @@
export * from './index.socket';

View 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);
}

View 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
View 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;
}
}

View 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
View 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
View 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();
}
}

View 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
View 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;
}
}

View 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) {}

View 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) {}

View 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;
}
}

View 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) {}

View 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) {}

View 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);

View File

View File

View 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
View 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) {};

View 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
View 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
View 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]

View 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

View File

@ -0,0 +1,7 @@
{
"name": "@avocado/math",
"version": "1.0.0",
"main": "index.js",
"author": "cha0s",
"license": "MIT"
}

View 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

View 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]

View 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
)

View 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]};
}

View 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

View 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

View 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

View 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
]

View 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
View 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';

View 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);
}
};
}

View 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);

View 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();
}
}
}

View File

@ -0,0 +1,7 @@
{
"name": "@avocado/mixins",
"version": "1.0.0",
"main": "index.js",
"author": "cha0s",
"license": "MIT"
}

View 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;
}
}

View 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;
}
};

View 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);
}
};
}

View 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);

View 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

View 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

View 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
View 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};

View 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

View 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"
}
}

View 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

View 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

View 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
View 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>;
}
}

View 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;
}

View 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);
}

View 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
View File

View 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
View 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
View 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 = {};

View File

@ -0,0 +1,7 @@
{
"name": "@avocado/state",
"version": "1.0.0",
"main": "index.js",
"author": "cha0s",
"license": "MIT"
}

View 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 dont 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]

View 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

View 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

View 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

View 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'

View File

@ -0,0 +1,7 @@
{
"name": "@avocado/timing",
"version": "1.0.0",
"main": "index.js",
"author": "cha0s",
"license": "MIT"
}

View 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

View 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
View File

@ -0,0 +1,3 @@
module.exports = {
mode: 'development',
};