<commit_before>"use strict";

// ---------------------------------------------------------------------------

const acx = require ('./acx.js')
const { ExchangeError, InsufficientFunds, OrderNotFound } = require ('./base/errors')

// ---------------------------------------------------------------------------

module.exports = class kuna extends acx {

    describe () {
        return this.deepExtend (super.describe (), {
            'id': 'kuna',
            'name': 'Kuna',
            'countries': 'UA',
            'rateLimit': 1000,
            'version': 'v2',
            'hasCORS': false,
            'hasFetchTickers': false,
            'hasFetchOHLCV': false,
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/1294454/31697638-912824fa-b3c1-11e7-8c36-cf9606eb94ac.jpg',
                'api': 'https://kuna.io',
                'www': 'https://kuna.io',
                'doc': 'https://kuna.io/documents/api',
            },
            'api': {
                'public': {
                    'get': [
                        'tickers/{market}',
                        'order_book',
                        'order_book/{market}',
                        'trades',
                        'trades/{market}',
                        'timestamp',
                    ],
                },
                'private': {
                    'get': [
                        'members/me',
                        'orders',
                        'trades/my',
                    ],
                    'post': [
                        'orders',
                        'order/delete',
                    ],
                },
            },
            'markets': {
                'BTC/UAH': { 'id': 'btcuah', 'symbol': 'BTC/UAH', 'base': 'BTC', 'quote': 'UAH', 'precision': { 'amount': 6, 'price': 0 }, 'lot': 0.000001, 'limits': { 'amount': { 'min': 0.000001, 'max': undefined }, 'price': { 'min': 1, 'max': undefined }}},
                'ETH/UAH': { 'id': 'ethuah', 'symbol': 'ETH/UAH', 'base': 'ETH', 'quote': 'UAH', 'precision': { 'amount': 6, 'price': 0 }, 'lot': 0.000001, 'limits': { 'amount': { 'min': 0.000001, 'max': undefined }, 'price': { 'min': 1, 'max': undefined }}},
                'GBG/UAH': { 'id': 'gbguah', 'symbol': 'GBG/UAH', 'base': 'GBG', 'quote': 'UAH', 'precision': { 'amount': 3, 'price': 2 }, 'lot': 0.001, 'limits': { 'amount': { 'min': 0.000001, 'max': undefined }, 'price': { 'min': 0.01, 'max': undefined }}}, // Golos Gold (GBG != GOLOS)
                'KUN/BTC': { 'id': 'kunbtc', 'symbol': 'KUN/BTC', 'base': 'KUN', 'quote': 'BTC', 'precision': { 'amount': 6, 'price': 6 }, 'lot': 0.000001, 'limits': { 'amount': { 'min': 0.000001, 'max': undefined }, 'price': { 'min': 0.000001, 'max': undefined }}},
                'BCH/BTC': { 'id': 'bchbtc', 'symbol': 'BCH/BTC', 'base': 'BCH', 'quote': 'BTC', 'precision': { 'amount': 6, 'price': 6 }, 'lot': 0.000001, 'limits': { 'amount': { 'min': 0.000001, 'max': undefined }, 'price': { 'min': 0.000001, 'max': undefined }}},
                'BCH/UAH': { 'id': 'bchuah', 'symbol': 'BCH/UAH', 'base': 'BCH', 'quote': 'UAH', 'precision': { 'amount': 6, 'price': 0 }, 'lot': 0.000001, 'limits': { 'amount': { 'min': 0.000001, 'max': undefined }, 'price': { 'min': 1, 'max': undefined }}},
                'WAVES/UAH': { 'id': 'wavesuah', 'symbol': 'WAVES/UAH', 'base': 'WAVES', 'quote': 'UAH', 'precision': { 'amount': 6, 'price': 0 }, 'lot': 0.000001, 'limits': { 'amount': { 'min': 0.000001, 'max': undefined }, 'price': { 'min': 1, 'max': undefined }}},
            },
            'fees': {
                'trading': {
                    'taker': 0.25 / 100,
                    'maker': 0.25 / 100,
                },
            },
        });
    }

    handleErrors (code, reason, url, method, headers, body) {
        if (code == 400) {
            let data = JSON.parse (body);
            let error = data['error'];
            let errorCode = error['code'];
            if (errorCode == 2002) {
                throw new InsufficientFunds ([ this.id, method, url, code, reason, body ].join (' '));
            } else if (errorCode == 2003) {
                throw new OrderNotFound ([ this.id, method, url, code, reason, body ].join (' '));
            }
        }
    }

    async fetchOrderBook (symbol, params = {}) {
        let market = this.market (symbol);
        let orderBook = await this.publicGetOrderBook (this.extend ({
            'market': market['id'],
        }, params));
        return this.parseOrderBook (orderBook, undefined, 'bids', 'asks', 'price', 'remaining_volume');
    }

    async fetchL3OrderBook (symbol, params) {
        return this.fetchOrderBook (symbol, params);
    }

    async fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
        if (!symbol)
            throw new ExchangeError (this.id + ' fetchOpenOrders requires a symbol argument');
        let market = this.market (symbol);
        let orders = await this.privateGetOrders (this.extend ({
            'market': market['id'],
        }, params));
        // todo emulation of fetchClosedOrders, fetchOrders, fetchOrder
        // with order cache + fetchOpenOrders
        // as in BTC-e, Liqui, Yobit, DSX, Tidex, WEX
        return this.parseOrders (orders, market, since, limit);
    }

    parseTrade (trade, market = undefined) {
        let timestamp = this.parse8601 (trade['created_at']);
        let symbol = undefined;
        if (market)
            symbol = market['symbol'];
        return {
            'id': trade['id'],
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'symbol': symbol,
            'type': undefined,
            'side': undefined,
            'price': parseFloat (trade['price']),
            'amount': parseFloat (trade['volume']),
            'info': trade,
        };
    }

    async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
        let market = this.market (symbol);
        let response = await this.publicGetTrades (this.extend ({
            'market': market['id'],
        }, params));
        return this.parseTrades (response, market, since, limit);
    }

    parseMyTrade (trade, market) {
        let timestamp = this.parse8601 (trade['created_at']);
        let symbol = undefined;
        if (market)
            symbol = market['symbol'];
        return {
            'id': trade['id'],
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'price': trade['price'],
            'amount': trade['volume'],
            'cost': trade['funds'],
            'symbol': symbol,
            'side': trade['side'],
            'order': trade['order_id'],
        };
    }

    parseMyTrades (trades, market = undefined) {
        let parsedTrades = [];
        for (let i = 0; i < trades.length; i++) {
            let trade = trades[i];
            let parsedTrade = this.parseMyTrade (trade, market);
            parsedTrades.push (parsedTrade);
        }
        return parsedTrades;
    }

    async fetchMyTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) {
        if (!symbol)
            throw new ExchangeError (this.id + ' fetchOpenOrders requires a symbol argument');
        let market = this.market (symbol);
        let response = await this.privateGetTradesMy ({ 'market': market['id'] });
        return this.parseMyTrades (response, market);
    }
}
<commit_msg>kuna: withdraw fees & markets<commit_after>"use strict";

// ---------------------------------------------------------------------------

const acx = require ('./acx.js')
const { ExchangeError, InsufficientFunds, OrderNotFound } = require ('./base/errors')

// ---------------------------------------------------------------------------

module.exports = class kuna extends acx {

    describe () {
        return this.deepExtend (super.describe (), {
            'id': 'kuna',
            'name': 'Kuna',
            'countries': 'UA',
            'rateLimit': 1000,
            'version': 'v2',
            'hasCORS': false,
            'hasFetchTickers': false,
            'hasFetchOHLCV': false,
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/1294454/31697638-912824fa-b3c1-11e7-8c36-cf9606eb94ac.jpg',
                'api': 'https://kuna.io',
                'www': 'https://kuna.io',
                'doc': 'https://kuna.io/documents/api',
                'fees': 'https://kuna.io/documents/api',
            },
            'api': {
                'public': {
                    'get': [
                        'tickers/{market}',
                        'order_book',
                        'order_book/{market}',
                        'trades',
                        'trades/{market}',
                        'timestamp',
                    ],
                },
                'private': {
                    'get': [
                        'members/me',
                        'orders',
                        'trades/my',
                    ],
                    'post': [
                        'orders',
                        'order/delete',
                    ],
                },
            },
            'markets': {
                'BTC/UAH': { 'id': 'btcuah', 'symbol': 'BTC/UAH', 'base': 'BTC', 'quote': 'UAH', 'precision': { 'amount': 6, 'price': 0 }, 'lot': 0.000001, 'limits': { 'amount': { 'min': 0.000001, 'max': undefined }, 'price': { 'min': 1, 'max': undefined }}},
                'ETH/UAH': { 'id': 'ethuah', 'symbol': 'ETH/UAH', 'base': 'ETH', 'quote': 'UAH', 'precision': { 'amount': 6, 'price': 0 }, 'lot': 0.000001, 'limits': { 'amount': { 'min': 0.000001, 'max': undefined }, 'price': { 'min': 1, 'max': undefined }}},
                'GBG/UAH': { 'id': 'gbguah', 'symbol': 'GBG/UAH', 'base': 'GBG', 'quote': 'UAH', 'precision': { 'amount': 3, 'price': 2 }, 'lot': 0.001, 'limits': { 'amount': { 'min': 0.000001, 'max': undefined }, 'price': { 'min': 0.01, 'max': undefined }}}, // Golos Gold (GBG != GOLOS)
                'KUN/BTC': { 'id': 'kunbtc', 'symbol': 'KUN/BTC', 'base': 'KUN', 'quote': 'BTC', 'precision': { 'amount': 6, 'price': 6 }, 'lot': 0.000001, 'limits': { 'amount': { 'min': 0.000001, 'max': undefined }, 'price': { 'min': 0.000001, 'max': undefined }}},
                'BCH/BTC': { 'id': 'bchbtc', 'symbol': 'BCH/BTC', 'base': 'BCH', 'quote': 'BTC', 'precision': { 'amount': 6, 'price': 6 }, 'lot': 0.000001, 'limits': { 'amount': { 'min': 0.000001, 'max': undefined }, 'price': { 'min': 0.000001, 'max': undefined }}},
                'BCH/UAH': { 'id': 'bchuah', 'symbol': 'BCH/UAH', 'base': 'BCH', 'quote': 'UAH', 'precision': { 'amount': 6, 'price': 0 }, 'lot': 0.000001, 'limits': { 'amount': { 'min': 0.000001, 'max': undefined }, 'price': { 'min': 1, 'max': undefined }}},
                'WAVES/UAH': { 'id': 'wavesuah', 'symbol': 'WAVES/UAH', 'base': 'WAVES', 'quote': 'UAH', 'precision': { 'amount': 6, 'price': 0 }, 'lot': 0.000001, 'limits': { 'amount': { 'min': 0.000001, 'max': undefined }, 'price': { 'min': 1, 'max': undefined }}},
                'ARN/BTC': { 'id': 'arnbtc', 'symbol': 'ARN/BTC', 'base': 'ARN', 'quote': 'BTC'},
                'B2B/BTC': { 'id': 'b2bbtc', 'symbol': 'B2B/BTC', 'base': 'B2B', 'quote': 'BTC'},
                'EVR/BTC': { 'id': 'evrbtc', 'symbol': 'EVR/BTC', 'base': 'EVR', 'quote': 'BTC'},
                'GOL/GBG': { 'id': 'golgbg', 'symbol': 'GOL/GBG', 'base': 'GOL', 'quote': 'GBG'},
                'R/BTC': { 'id': 'rbtc', 'symbol': 'R/BTC', 'base': 'R', 'quote': 'BTC'},
                'RMC/BTC': { 'id': 'rmcbtc', 'symbol': 'RMC/BTC', 'base': 'RMC', 'quote': 'BTC'},
            },
            'fees': {
                'trading': {
                    'taker': 0.25 / 100,
                    'maker': 0.25 / 100,
                },
                'funding': {
                    'withdraw': {
                        'UAH': '1%',
                        'BTC': 0.001,
                        'BCH': 0.001,
                        'ETH': 0.01,
                        'WAVES': 0.01,
                        'GOL': 0.0,
                        'GBG': 0.0,
                        // 'RMC': 0.001 BTC
                        // 'ARN': 0.01 ETH
                        // 'R': 0.01 ETH
                        // 'EVR': 0.01 ETH
                    },
                    'deposit': {
                        // 'UAH': (amount) => amount * 0.001 + 5
                    },
                },
            },
        });
    }

    handleErrors (code, reason, url, method, headers, body) {
        if (code == 400) {
            let data = JSON.parse (body);
            let error = data['error'];
            let errorCode = error['code'];
            if (errorCode == 2002) {
                throw new InsufficientFunds ([ this.id, method, url, code, reason, body ].join (' '));
            } else if (errorCode == 2003) {
                throw new OrderNotFound ([ this.id, method, url, code, reason, body ].join (' '));
            }
        }
    }

    async fetchOrderBook (symbol, params = {}) {
        let market = this.market (symbol);
        let orderBook = await this.publicGetOrderBook (this.extend ({
            'market': market['id'],
        }, params));
        return this.parseOrderBook (orderBook, undefined, 'bids', 'asks', 'price', 'remaining_volume');
    }

    async fetchL3OrderBook (symbol, params) {
        return this.fetchOrderBook (symbol, params);
    }

    async fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
        if (!symbol)
            throw new ExchangeError (this.id + ' fetchOpenOrders requires a symbol argument');
        let market = this.market (symbol);
        let orders = await this.privateGetOrders (this.extend ({
            'market': market['id'],
        }, params));
        // todo emulation of fetchClosedOrders, fetchOrders, fetchOrder
        // with order cache + fetchOpenOrders
        // as in BTC-e, Liqui, Yobit, DSX, Tidex, WEX
        return this.parseOrders (orders, market, since, limit);
    }

    parseTrade (trade, market = undefined) {
        let timestamp = this.parse8601 (trade['created_at']);
        let symbol = undefined;
        if (market)
            symbol = market['symbol'];
        return {
            'id': trade['id'],
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'symbol': symbol,
            'type': undefined,
            'side': undefined,
            'price': parseFloat (trade['price']),
            'amount': parseFloat (trade['volume']),
            'info': trade,
        };
    }

    async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
        let market = this.market (symbol);
        let response = await this.publicGetTrades (this.extend ({
            'market': market['id'],
        }, params));
        return this.parseTrades (response, market, since, limit);
    }

    parseMyTrade (trade, market) {
        let timestamp = this.parse8601 (trade['created_at']);
        let symbol = undefined;
        if (market)
            symbol = market['symbol'];
        return {
            'id': trade['id'],
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'price': trade['price'],
            'amount': trade['volume'],
            'cost': trade['funds'],
            'symbol': symbol,
            'side': trade['side'],
            'order': trade['order_id'],
        };
    }

    parseMyTrades (trades, market = undefined) {
        let parsedTrades = [];
        for (let i = 0; i < trades.length; i++) {
            let trade = trades[i];
            let parsedTrade = this.parseMyTrade (trade, market);
            parsedTrades.push (parsedTrade);
        }
        return parsedTrades;
    }

    async fetchMyTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) {
        if (!symbol)
            throw new ExchangeError (this.id + ' fetchOpenOrders requires a symbol argument');
        let market = this.market (symbol);
        let response = await this.privateGetTradesMy ({ 'market': market['id'] });
        return this.parseMyTrades (response, market);
    }
}
<|endoftext|>


<commit_before>var async = require('async');
var validator = require('validator/lib/validators');
var uuid = require('node-uuid');
var utils = require('prana').utils;

var field = module.exports = {};

/**
 * The field() hook.
 */
field.field = function(fields, callback) {
  var newFields = {};

  newFields['id'] = {
    title: 'Identifier',
    description: 'A Universally unique identifier.',
    schema: {
      type: 'string',
      uuid: true
    },
    preSave: function(settings, item, next) {
      if (!(settings.name in item)) {
        // Generate an UUID v4.
        item[settings.name] = uuid.v4();
      }
      next();
    }
  };
  newFields['text'] = {
    title: 'Text',
    schema: function(settings) {
      var schema = {
        type: (settings.maxLength > 256) ? 'text' : 'string'
      };

      // Add max/min properties if set.
      ['maxLength', 'minLength'].map(function(what) {
        if (what in settings) {
          schema[what] = settings[what];
        }
      });

      return schema;
    },
    element: 'text',
    validate: function(settings, item, next) {
      // Default minLenght to 1.
      settings.minLength = settings.minLength || 1;

      // Default maxLenght to 256.
      settings.maxLength = settings.maxLength || 256;

      next(null, !validator.notEmpty(item[settings.name]) || validator.len(item[settings.name].toString(), settings.minLength, settings.maxLength) || 'Value must have from ' + settings.minLength + ' to ' + settings.maxLength + ' characters.');
    }
  };
  newFields['number'] = {
    title: 'Number',
    schema: function(settings) {
      var schema = {
        type: 'number'
      };

      // Add max/min properties if set.
      ['max', 'min'].map(function(what) {
        if (what in settings) {
          schema[what] = settings[what];
        }
      });

      return schema;
    },
    element: 'number',
    validate: function(settings, item, next) {
      next(null, validator.isNumeric(item[settings.name].toString()) || 'Invalid number.');
    }
  };
  newFields['date'] = {
    title: 'Date',
    schema: 'date',
    element: 'date',
    validate: function(settings, item, next) {
      next(null, validator.isDate(item[settings.name].toString()) || 'Invalid date.');
    }
  };
  newFields['boolean'] = {
    title: 'Boolean',
    description: 'True or false, yes or no, on or off.',
    schema: 'boolean',
    element: 'checkbox'
  };
  newFields['email'] = {
    title: 'Email',
    schema: {
      type: 'string',
      email: true
    },
    element: 'email',
    validate: function(settings, item, next) {
      // Email validator oddly returns the email itself, so need to convert to
      // boolean.
      next(null, new Boolean(validator.isEmail(item[settings.name].toString())) || 'Invalid email.');
    }
  };
  newFields['url'] = {
    title: 'URL',
    schema: {
      type: 'string',
      url: true
    },
    element: 'url',
    validate: function(settings, item, next) {
      next(null, validator.isUrl(item[settings.name].toString()) || 'Invalid URL.');
    }
  };
  newFields['telephone'] = {
    title: 'Telephone',
    schema: {
      type: 'string'
    },
    element: 'tel'
  };
  newFields['password'] = {
    title: 'Password',
    element: 'password',
    validate: function(settings, item, next) {
      var minLength = settings.minLength || 6;
      next(null, validator.len(item[settings.name].toString(), settings.minLength || 6) || 'Password must have at least ' + minLength + ' characters.');
    }
  };

  callback(null, newFields);
};

/**
 * Generate a hook implementation for each pre/post operations that calls a
 * callback on the field type with the same name.
 */
field.fieldCallback = function(hook) {
  return function(type, data, callback) {
    var application = this.application;

    if (type.settings.fields) {
      // Validate type fields.
      async.each(Object.keys(type.settings.fields), function(fieldName, next) {
        var fieldSettings = type.settings.fields[fieldName];

        // Add fieldName to fieldSettings.
        fieldSettings.name = fieldName;

        self.application.pick('field', fieldSettings.type, function(error, field) {
          if (error) {
            // Application error.
            return next(error);
          }
          if (!field || !(hook in field)) {
            // Field is of an unrecognized type or there's not a preSave()
            // callback.
            return next();
          }

          field[hook](fieldSettings, data, function(error, result) {
            if (error) {
              // Application error.
              return next(error);
            }

            next();
          });
        });
      },
      function(error) {
        if (error) {
          // Application error.
          return callback(error);
        }
        callback();
      });
    }
    else {
      callback();
    }
  };
};

/**
 * Create hook implementations for all type operations to call field hooks.
 */
['load', 'list', 'save', 'delete'].forEach(function(operation) {
  var operationCapitalized = utils.capitalizeFirstLetter(operation);

  // Add pre operation hooks that call callbacks on fields.
  ['pre', 'post'].forEach(function(kind) {
    var hook = kind + operationCapitalized;
    field[hook] = field.fieldCallback(hook);
  });
});
<commit_msg>Fixing number field, making it declare the attribute either as a integer or float depending on the number of decimals. #147<commit_after>var async = require('async');
var validator = require('validator/lib/validators');
var uuid = require('node-uuid');
var utils = require('prana').utils;

var field = module.exports = {};

/**
 * The field() hook.
 */
field.field = function(fields, callback) {
  var newFields = {};

  newFields['id'] = {
    title: 'Identifier',
    description: 'A Universally unique identifier.',
    schema: {
      type: 'string',
      uuid: true
    },
    preSave: function(settings, item, next) {
      if (!(settings.name in item)) {
        // Generate an UUID v4.
        item[settings.name] = uuid.v4();
      }
      next();
    }
  };
  newFields['text'] = {
    title: 'Text',
    schema: function(settings) {
      var schema = {
        type: (settings.maxLength > 256) ? 'text' : 'string'
      };

      // Add max/min properties if set.
      ['maxLength', 'minLength'].map(function(what) {
        if (what in settings) {
          schema[what] = settings[what];
        }
      });

      return schema;
    },
    element: 'text',
    validate: function(settings, item, next) {
      // Default minLenght to 1.
      settings.minLength = settings.minLength || 1;

      // Default maxLenght to 256.
      settings.maxLength = settings.maxLength || 256;

      next(null, !validator.notEmpty(item[settings.name]) || validator.len(item[settings.name].toString(), settings.minLength, settings.maxLength) || 'Value must have from ' + settings.minLength + ' to ' + settings.maxLength + ' characters.');
    }
  };
  newFields['number'] = {
    title: 'Number',
    schema: function(settings) {
      var schema = {
        type: (settings.decimals && settings.decimals > 0) ? 'float' : 'integer'
      };

      // Add max/min properties if set.
      ['max', 'min'].map(function(what) {
        if (what in settings) {
          schema[what] = settings[what];
        }
      });

      return schema;
    },
    element: 'number',
    validate: function(settings, item, next) {
      next(null, validator.isNumeric(item[settings.name].toString()) || 'Invalid number.');
    }
  };
  newFields['date'] = {
    title: 'Date',
    schema: 'date',
    element: 'date',
    validate: function(settings, item, next) {
      next(null, validator.isDate(item[settings.name].toString()) || 'Invalid date.');
    }
  };
  newFields['boolean'] = {
    title: 'Boolean',
    description: 'True or false, yes or no, on or off.',
    schema: 'boolean',
    element: 'checkbox'
  };
  newFields['email'] = {
    title: 'Email',
    schema: {
      type: 'string',
      email: true
    },
    element: 'email',
    validate: function(settings, item, next) {
      // Email validator oddly returns the email itself, so need to convert to
      // boolean.
      next(null, new Boolean(validator.isEmail(item[settings.name].toString())) || 'Invalid email.');
    }
  };
  newFields['url'] = {
    title: 'URL',
    schema: {
      type: 'string',
      url: true
    },
    element: 'url',
    validate: function(settings, item, next) {
      next(null, validator.isUrl(item[settings.name].toString()) || 'Invalid URL.');
    }
  };
  newFields['telephone'] = {
    title: 'Telephone',
    schema: {
      type: 'string'
    },
    element: 'tel'
  };
  newFields['password'] = {
    title: 'Password',
    element: 'password',
    validate: function(settings, item, next) {
      var minLength = settings.minLength || 6;
      next(null, validator.len(item[settings.name].toString(), settings.minLength || 6) || 'Password must have at least ' + minLength + ' characters.');
    }
  };

  callback(null, newFields);
};

/**
 * Generate a hook implementation for each pre/post operations that calls a
 * callback on the field type with the same name.
 */
field.fieldCallback = function(hook) {
  return function(type, data, callback) {
    var application = this.application;

    if (type.settings.fields) {
      // Validate type fields.
      async.each(Object.keys(type.settings.fields), function(fieldName, next) {
        var fieldSettings = type.settings.fields[fieldName];

        // Add fieldName to fieldSettings.
        fieldSettings.name = fieldName;

        self.application.pick('field', fieldSettings.type, function(error, field) {
          if (error) {
            // Application error.
            return next(error);
          }
          if (!field || !(hook in field)) {
            // Field is of an unrecognized type or there's not a preSave()
            // callback.
            return next();
          }

          field[hook](fieldSettings, data, function(error, result) {
            if (error) {
              // Application error.
              return next(error);
            }

            next();
          });
        });
      },
      function(error) {
        if (error) {
          // Application error.
          return callback(error);
        }
        callback();
      });
    }
    else {
      callback();
    }
  };
};

/**
 * Create hook implementations for all type operations to call field hooks.
 */
['load', 'list', 'save', 'delete'].forEach(function(operation) {
  var operationCapitalized = utils.capitalizeFirstLetter(operation);

  // Add pre operation hooks that call callbacks on fields.
  ['pre', 'post'].forEach(function(kind) {
    var hook = kind + operationCapitalized;
    field[hook] = field.fieldCallback(hook);
  });
});
<|endoftext|>


<commit_before>var L20n = {
  getContext: function() {
    return new this.Context();
  },
  _startLoading: function(res, callback) {
    var xhr = new XMLHttpRequest();
    xhr.overrideMimeType('text/plain');
    xhr.addEventListener("load", function() {
        return callback(xhr.responseText)
    });
    xhr.open('GET', res.uri, true);
    xhr.send('');
  },
  _paths: {'system': '/l20n/js/data/sys.j20n',
           'globals': '/l20n/js/data/default.j20n'},
}

L20n.Resource = function(aURI) {
  this.uri = aURI;
}

L20n.Resource.prototype = {
  _loading: true,
  uri: null,
}

L20n.Context = function() {
  mFrozen = false;
  mResources = [];
  mEvents = {'ready': []}

  mObjects = {
    'resources': {},
    'context': {},
    'system': {},
    'globals': {},
  }

  for (var name in L20n._paths)
    this._getObject(mObjects[name], L20n._paths[name]);
}

L20n.Context.prototype = {
  addResource: function(aURI) {
    var res = this._getObject(mObjects['resources'], aURI);
  },
  get: function(id, args) {
    var curObj = this._get(id, args);
    return mObjects['system'].getent(curObj, mObjects['system'], id);
  },
  getAttributes: function(id, args) {
    var curObj = this._get(id, args);
    return mObjects['system'].getattrs(curObj, mObjects['system'], id);
  },
  isFrozen: function() {
    return mFrozen;
  },
  freeze: function() {
    mFrozen = true;
    if (this.isReady()) {
      this._fireCallback();
    }
  },
  isReady: function() {
    if (!mFrozen)
      return false;
    for (var i = 0; i < mResources.length; i++) {
      if (mResources[i]._loading)
        return false;
    }
    return true;
  },
  set onReady(callback) {
    if (!this.isReady())
      mEvents['ready'].push(callback);
    else
      callback();
  },
  set data(data) {
    mObjects['context'] = data
  },
  get data() {
    return mObjects['context'];
  },

  // Private
  _get: function(id, args) {
    var curObj = mObjects['resources'];
    if (mObjects['context']) {
      mObjects['context'].__proto__ = curObj;
      curObj = mObjects['context'];
    }
    if (args) {
      args.__proto__ = curObj;
      curObj = args;
    }
    mObjects['globals'].__proto__ = curObj;
    return mObjects['globals'];
  },
  _loadObject: function(data, obj) {
    new Function(data).call(obj);
  },
  _getObject: function(obj, url) {
    var self = this;
    var res = new L20n.Resource(url);
    var _injectResource = function(data) {
      self._loadObject(data, obj);
      res._loading = false;

      if (self.isReady()) {
        self._fireCallback();
      }
    }
    L20n._startLoading(res, _injectResource);
    mResources.push(res);
  },
  _fireCallback: function() {
    if (mEvents['ready'].length) {
      for (var i in mEvents['ready'])
        mEvents['ready'][i]();
      mEvents['ready'] = [];
    }
  }
}

<commit_msg>update L20n lib to use parser/compiler<commit_after>var L20n = {
  getContext: function() {
    return new this.Context();
  },
  _startLoading: function(res, callback) {
    var xhr = new XMLHttpRequest();
    xhr.overrideMimeType('text/plain');
    xhr.addEventListener("load", function() {
        return callback(xhr.responseText)
    });
    xhr.open('GET', res.uri, true);
    xhr.send('');
  }
}

L20n.Resource = function(aURI) {
  this.uri = aURI;
}

L20n.Resource.prototype = {
  _loading: true,
  uri: null,
}

L20n.Context = function() {
  mFrozen = false;
  mResources = [];
  mEvents = {'ready': []}

  mObjects = {
    'resources': {},
    'context': {},
    'globals': {},
  }

}

L20n.Context.prototype = {
  addResource: function(aURI) {
    var res = this._getObject(mObjects['resources'], aURI);
  },
  get: function(id, args) {
    return mObjects['resources'][id]();
    var curObj = this._get(id, args);
    return mObjects['system'].getent(curObj, mObjects['system'], id);
  },
  getAttributes: function(id, args) {
    var curObj = this._get(id, args);
    return mObjects['system'].getattrs(curObj, mObjects['system'], id);
  },
  isFrozen: function() {
    return mFrozen;
  },
  freeze: function() {
    mFrozen = true;
    if (this.isReady()) {
      this._fireCallback();
    }
  },
  isReady: function() {
    if (!mFrozen)
      return false;
    for (var i = 0; i < mResources.length; i++) {
      if (mResources[i]._loading)
        return false;
    }
    return true;
  },
  set onReady(callback) {
    if (!this.isReady())
      mEvents['ready'].push(callback);
    else
      callback();
  },
  set data(data) {
    mObjects['context'] = data
  },
  get data() {
    return mObjects['context'];
  },

  // Private
  _get: function(id, args) {
    var curObj = mObjects['resources'];
    if (mObjects['context']) {
      mObjects['context'].__proto__ = curObj;
      curObj = mObjects['context'];
    }
    if (args) {
      args.__proto__ = curObj;
      curObj = args;
    }
    mObjects['globals'].__proto__ = curObj;
    return mObjects['globals'];
  },
  _loadObject: function(data, obj) {
    var ast = Parser.parse(data);
    Compiler.compile(ast, obj);
    //new Function(data).call(obj);
  },
  _getObject: function(obj, url) {
    var self = this;
    var res = new L20n.Resource(url);
    var _injectResource = function(data) {
      self._loadObject(data, obj);
      res._loading = false;
      if (self.isReady()) {
        self._fireCallback();
      }
    }
    L20n._startLoading(res, _injectResource);
    mResources.push(res);
  },
  _fireCallback: function() {
    if (mEvents['ready'].length) {
      for (var i in mEvents['ready'])
        mEvents['ready'][i]();
      mEvents['ready'] = [];
    }
  }
}

<|endoftext|>


<commit_before>/*
*    Lynx Project
*    Started August 2013
*    ------------------------------------------------------
*    This file is covered under the LynxJS Game Library
*    License. Please read license.txt for more information
*    on usage of this library.
*    ------------------------------------------------------
*    File Name: Line.js
*    Description: Two points on the Lynx Map. Actually a Line Segment
*    Global Variables: Lynx.Line, Lynx.L
*/

Lynx.Line = function(pA, pB){
	var that = {};

	that.A = pA;
	that.B = pB;

	/**
	* Description: Finds the Length of the line Segment
	*
	* @this {Lynx.Line}
	* @return {int} the Length of the segment.
	*/
	Object.defineProperty(that, "Length", {
		get: function(){
			return Math.sqrt((that.A.X - that.B.X)^2 + (that.A.Y - that.B.Y)^2);
		}
	});

	/**
	* Description: Finds the Slope of the line
	*
	* @this {Lynx.Line}
	* @return {int} The Slope of the line
	*/
	Object.defineProperty(that, "Slope", {
		get: function(){
			if(this.A.X == this.B.X)
				return Infinity;
			if(this.A.Y == this.B.Y)
				return 0;
			if(this.A.X > this.B.X)
				return ((this.B.Y - this.A.Y)/(this.B.X - this.A.X));

			return ((this.A.Y - that.B.Y)/(that.A.X - this.B.X));
		}
	});

	/**
	* Description: Finds the Y Intercept of the line
	*
	* @this {Lynx.Line}
	* @return {int} Y Intercept value
	*/
	Object.defineProperty(that, "Intercept", {
		get: function(){
			return (this.A.Y - (this.Slope * this.A.X));
		}
	})

	/**
	* Description: Tests whether a given point lies on the line segment
	*
	* @this {Lynx.Line}
	* @param {Lynx.Point} <pPoint> The point to test
	* @return {boolean} Whether or not the point is on the line segment.
	*/
	that.IsOnLine = (function(pPoint)
	{
		if(this.A.X < this.B.X)
		{
			if(!(this.A.X <= pPoint.X && this.B.X >= pPoint.X))
				return false;
		}
		else
		{
			if(!(this.A.X >= pPoint.X && this.B.X <= pPoint.X))
				return false;
		}

		if(this.A.Y < this.B.Y)
		{
			if(!(this.A.Y <= pPoint.Y && this.B.Y >= pPoint.Y))
				return false;
		}
		else
		{
			if(!(this.A.Y >= pPoint.Y && this.B.Y <= pPoint.Y))
				return false;
		}

		//Definitely in bounds, now let's make sure it's on the line itself.
		return (pPoint.Y == (this.Slope * pPoint.X + this.Intercept));
	}).bind(that);
}

Lynx.L = function(pX, pY){ return Lynx.Line(pX, pY); }
<commit_msg>Fixes minor bugs in Line.js that would cause a horrible fate for any browser attempting to find the intercept of a line with an infinite slope.<commit_after>/*
*    Lynx Project
*    Started August 2013
*    ------------------------------------------------------
*    This file is covered under the LynxJS Game Library
*    License. Please read license.txt for more information
*    on usage of this library.
*    ------------------------------------------------------
*    File Name: Line.js
*    Description: Two points on the Lynx Map. Actually a Line Segment
*    Global Variables: Lynx.Line, Lynx.L
*/

Lynx.Line = function(pA, pB){
	var that = {};

	that.A = pA;
	that.B = pB;

	/**
	* Description: Finds the Length of the line Segment
	*
	* @this {Lynx.Line}
	* @return {int} the Length of the segment.
	*/
	Object.defineProperty(that, "Length", {
		get: function(){
			return Math.sqrt((that.A.X - that.B.X)^2 + (that.A.Y - that.B.Y)^2);
		}
	});

	/**
	* Description: Finds the Slope of the line
	*
	* @this {Lynx.Line}
	* @return {int} The Slope of the line
	*/
	Object.defineProperty(that, "Slope", {
		get: function(){
			if(this.A.X == this.B.X)
				return Infinity;
			if(this.A.Y == this.B.Y)
				return 0;
			if(this.A.X > this.B.X)
				return ((this.B.Y - this.A.Y)/(this.B.X - this.A.X));

			return ((this.A.Y - that.B.Y)/(that.A.X - this.B.X));
		}
	});

	/**
	* Description: Finds the Y Intercept of the line
	*
	* @this {Lynx.Line}
	* @return {int} Y Intercept value
	*/
	Object.defineProperty(that, "Intercept", {
		get: function(){
			if(this.Slope === Infinity)
				return 0; //Default to 0 bc impossible.

			return (this.A.Y - (this.Slope * this.A.X));
		}
	})

	/**
	* Description: Tests whether a given point lies on the line segment
	*
	* @this {Lynx.Line}
	* @param {Lynx.Point} <pPoint> The point to test
	* @return {boolean} Whether or not the point is on the line segment.
	*/
	that.IsOnLine = (function(pPoint)
	{
		if(this.A.X < this.B.X)
		{
			if(!(this.A.X <= pPoint.X && this.B.X >= pPoint.X))
				return false;
		}
		else
		{
			if(!(this.A.X >= pPoint.X && this.B.X <= pPoint.X))
				return false;
		}

		if(this.A.Y < this.B.Y)
		{
			if(!(this.A.Y <= pPoint.Y && this.B.Y >= pPoint.Y))
				return false;
		}
		else
		{
			if(!(this.A.Y >= pPoint.Y && this.B.Y <= pPoint.Y))
				return false;
		}

		//Definitely in bounds, now let's make sure it's on the line itself.
		if(this.Slope === Infinity)
			return pPoint.X == this.A.X;

		return (pPoint.Y == (this.Slope * pPoint.X + this.Intercept));
	}).bind(that);

	return that;
}

Lynx.L = function(pA, pB){ return Lynx.Line(pA, pB); }
<|endoftext|>


<commit_before>'use strict';

import dom from 'bower:metal/src/dom/dom';
import SoyComponent from 'bower:metal/src/soy/SoyComponent';
import ComponentRegistry from 'bower:metal/src/component/ComponentRegistry';
import './ListItem.js';
import './List.soy.js';

/**
 * List component.
 */
class List extends SoyComponent {
	constructor(opt_config) {
		super(opt_config);
	}

	/**
	 * Handles click event on the list. The function fires an
	 * {@code itemSelected} event.
	 *
	 * @protected
	 * @param {Event} event The native click event
	 */
	handleClick(event) {
		var target = event.target;
		while (target) {
			if (dom.match(target, '.listitem')) {
				break;
			}
			target = target.parentNode;
		}
		this.emit('itemSelected', target);
	}
}

/**
 * Default list elementClasses.
 * @default list
 * @type {String}
 * @static
 */
List.ELEMENT_CLASSES = 'list';

/**
 * List attributes definition.
 * @type {Object}
 * @static
 */
List.ATTRS = {
	/**
	 * The list items. Each is represented by an object that can have the following keys:
	 *   - textPrimary: The item's main content.
	 *   - textSecondary: (Optional) The item's help content.
	 *   - icons: (Optional) A list of icon css classes to render on the right side.
	 *   - iconsHtml: (Optional) A list of icon css classes to render on the right side.
	 *   - avatar: (Optional) An object that specifies the avatar's content and, optionally, a css
	 *       class it should use.
	 * @type {!Array<!Object>}
	 * @default []
	 */
	items: {
		validator: Array.isArray,
		valueFn: function() {
			return [];
		}
	},

	itemsHtml: {
	}
};

ComponentRegistry.register('List', List);

export default List;
<commit_msg>Fixes documentation to fit the standard<commit_after>'use strict';

import dom from 'bower:metal/src/dom/dom';
import SoyComponent from 'bower:metal/src/soy/SoyComponent';
import ComponentRegistry from 'bower:metal/src/component/ComponentRegistry';
import './ListItem.js';
import './List.soy.js';

/**
 * List component.
 */
class List extends SoyComponent {
	/**
	 * @inheritDoc
	 */
	constructor(opt_config) {
		super(opt_config);
	}

	/**
	 * Handles click event on the list. The function fires an
	 * {@code itemSelected} event.
	 * @param {!Event} event The native click event
	 */
	handleClick(event) {
		var target = event.target;
		while (target) {
			if (dom.match(target, '.listitem')) {
				break;
			}
			target = target.parentNode;
		}
		this.emit('itemSelected', target);
	}
}

/**
 * Default list elementClasses.
 * @default list
 * @type {string}
 * @static
 */
List.ELEMENT_CLASSES = 'list';

/**
 * List attributes definition.
 * @type {!Object}
 * @static
 */
List.ATTRS = {
	/**
	 * The list items. Each is represented by an object that can have the following keys:
	 *   - textPrimary: The item's main content.
	 *   - textSecondary: (Optional) The item's help content.
	 *   - icons: (Optional) A list of icon css classes to render on the right side.
	 *   - iconsHtml: (Optional) A list of icon css classes to render on the right side.
	 *   - avatar: (Optional) An object that specifies the avatar's content and, optionally, a css
	 *       class it should use.
	 * @type {!Array<!Object>}
	 * @default []
	 */
	items: {
		validator: Array.isArray,
		valueFn: function() {
			return [];
		}
	},

	/**
	 * The list items as HTML to be added directly to the list.
	 * @type {string}
	 */
	itemsHtml: {
	}
};

ComponentRegistry.register('List', List);

export default List;
<|endoftext|>


<commit_before>
function print(){
    document.write('
');

    _.times(8, function(){
        document.write('
');
        _.times(8, function(){
            document.write('	');
        });
        document.write('
');
    });

    document.write('
');
}

print();
<commit_msg>css and js merged.  its a start<commit_after>function print(){
    document.write('
');

    _.times(8, function(){
        document.write('
');
        _.times(8, function(){
            document.write('	');
        });
        document.write('
');
    });

    document.write('
');
}

print();
<|endoftext|>


<commit_before>var jsfx = {};
(function () {
    var Parameters = [];

    this.loadParameters = function () {
        var grp = 0;
        var ap = function (name, min, max, def) {
            var param = { name: name,
                          min: min, max: max, def: def,
                          node: undefined, group: grp};
            Parameters.push(param);
        };

        ap("Master Volume",  0, 1, 0.3);
        grp++;

        ap("Attack Time",    0, 1, 0.1); // seconds
        ap("Sustain Time",   0, 2, 0.4); // seconds
        ap("Sustain Punch",  0, 3, 2);
        ap("Decay Time",     0, 2, 1); // seconds
        grp++;

        ap("Min Frequency",   20, 2400, 0);
        ap("Start Frequency", 20, 2400, 440);
        ap("Max Frequency",   20, 2400, 2000);
        ap("Slide",           -1, 1, 0);
        ap("Delta Slide",     -1, 1, 0);

        grp++;
        ap("Vibrato Depth",     0, 1, 0.1);
        ap("Vibrato Frequency", 0.01, 48, 8);
        ap("Vibrato Depth Slide",   -0.3, 1, 0);
        ap("Vibrato Frequency Slide", -1, 1, 0);

        grp++;
        ap("Change Amount", -12, 12, 0);
        ap("Change Speed",  0, 2, 0.1);

        grp++;
        ap("Square Duty", 0, 100, 50);
        ap("Duty Sweep",  0, 100, 50);

        grp++;
        ap("Repeat Speed", 0, 100, 50);

        grp++;
        ap("Phaser Offset", 0, 100, 50);
        ap("Phaser Sweep",  0, 100, 50);

        grp++;
        ap("LP Filter Cutoff", 0, 100, 50);
        ap("LP Filter Cutoff Sweep", 0, 100, 50);
        ap("LP Filter Resonance",    0, 100, 50);
        ap("HP Filter Cutoff",       0, 100, 50);
        ap("HP Filter Cutoff Sweep", 0, 100, 50);
    };

    this.generate = function(params){
        // useful consts
        var TAU = 2 * Math.PI;
        var SampleRate = audio.SampleRate;

        // enveloping initialization
        var _ss = 1.0 + params.SustainPunch;
        var envelopes = [ {from: 0.0, to: 1.0, time: params.AttackTime},
                          {from: _ss, to: 1.0, time: params.SustainTime},
                          {from: 1.0, to: 0.0, time: params.DecayTime}];
        var envelopes_len = envelopes.length;

        // envelope sample calculation
        for(var i = 0; i < envelopes_len; i++){
            envelopes[i].samples = (envelopes[i].time * SampleRate) | 0
        }

        // envelope loop variables
        var envelope = undefined;
        var envelope_cur = 0.0;
        var envelope_idx = -1;
        var envelope_increment = 0.0;
        var envelope_last = -1;

        // count total samples
        var totalSamples = 0;
        for(var i = 0; i < envelopes_len; i++){
            totalSamples += envelopes[i].samples;
        }

        // out data samples
        var out = new Array(totalSamples);
        var sample = 0;

        // main generator
        var generator = params.generator;
        var generator_A = 0;
        var generator_B = 0;

        // phase calculation
        var phase = 0;
        var phase_speed = params.StartFrequency * TAU / SampleRate;

        // frequency limiter
        if(params.MinFrequency > params.StartFrequency)
            params.MinFrequency = params.StartFrequency;

        if(params.MaxFrequency < params.StartFrequency)
            params.MaxFrequency = params.StartFrequency;

        var phase_min_speed = params.MinFrequency * TAU / SampleRate;
        var phase_max_speed = params.MaxFrequency * TAU / SampleRate;

        // frequency vibrato
        var vibrato_phase = 0;
        var vibrato_phase_speed = params.VibratoFrequency * TAU / SampleRate;
        var vibrato_amplitude = params.VibratoDepth;

        // frequency vibrato slide
        var vibrato_phase_slide = 1.0 + Math.pow(params.VibratoFrequencySlide, 3.0) * 3.0 / SampleRate;
        var vibrato_amplitude_slide = params.VibratoDepthSlide / SampleRate;

        // slide calculation
        var slide = 1.0 + Math.pow(params.Slide, 3.0) * 3.0 / SampleRate;
        var delta_slide = Math.pow(params.DeltaSlide, 3.0) / (SampleRate * 1000);

        // arpeggiator
        var arpeggiator_time = 0;
        var arpeggiator_limit = params.ChangeSpeed * SampleRate;
        var arpeggiator_mod   = 1 + (params.ChangeAmount | 0) / 12.0;
        console.debug(arpeggiator_mod);

        // master volume controller
        var master_volume = params.MasterVolume;

        for(var i = 0; i < totalSamples; i++){
            // main generator
            sample = generator(phase, generator_A, generator_B);

            // phase calculation
            phase += phase_speed;

            // phase slide calculation
            slide += delta_slide;
            phase_speed *= slide;

            // arpeggiator
            if ( arpeggiator_time > arpeggiator_limit ){
                phase_speed *= arpeggiator_mod;
                arpeggiator_time = 0;
                // arpeggiator_limit = totalSamples * 2;
            }
            arpeggiator_time += 1;

            // frequency limiter
            if (phase_speed > phase_max_speed){
                phase_speed = phase_max_speed;
            } else if(phase_speed < phase_min_speed){
                phase_speed = phase_min_speed;
            }

            // frequency vibrato
            vibrato_phase += vibrato_phase_speed;
            var _vibrato_phase_mod = phase_speed * Math.sin(vibrato_phase) * vibrato_amplitude;
            phase += _vibrato_phase_mod;

            // frequency vibrato slide
            vibrato_phase_speed *= vibrato_phase_slide;
            if(vibrato_amplitude_slide !== 0){
                vibrato_amplitude += vibrato_amplitude_slide;
                if(vibrato_amplitude < 0){
                    vibrato_amplitude = 0;
                    vibrato_amplitude_slide = 0;
                } else if (vibrato_amplitude > 1){
                    vibrato_amplitude = 1;
                    vibrato_amplitude_slide = 0;
                }
            }

            // envelope processing
            if( i > envelope_last ){
                envelope_idx += 1;
                if(envelope_idx < envelopes_len) // fault protection
                    envelope = envelopes[envelope_idx];
                envelope_cur = envelope.from;
                envelope_increment = (envelope.to - envelope.from) / envelope.samples;
                envelope_last += envelope.samples;
            }
            sample *= envelope_cur;
            envelope_cur += envelope_increment;

            // master volume controller
            sample *= master_volume;

            // prepare for next sample
            out[i] = sample;
        }
        return out;
    }

    this.createConfigurationPanel = function (id) {
        this.loadParameters();
        var frag = document.createDocumentFragment(),
            len = Parameters.length,
            lastgroup = undefined;
        var group;
        for (var i = 0; i < len; i += 1) {
            var param = Parameters[i],
                row = document.createElement("tr"),
                fld = document.createElement("td"),
                input = document.createElement("input");
            if (param.group !== lastgroup) {
                group = document.createElement("table");
                frag.appendChild(group);
                lastgroup = param.group;
            }
            // add caption
            fld.appendChild(document.createTextNode(param.name));
            row.appendChild(fld);
            // create sliders
            fld = document.createElement("td");
            input.type = "range";
            input.step = (param.max - param.min) / 1000.0;
            input.min  = param.min;
            input.max  = param.max;
            input.value = param.def;
            param.node = input;
            fld.appendChild(input);
            row.appendChild(fld);
            group.appendChild(row);
        }
        var confpanel = document.getElementById(id);
        confpanel.appendChild(frag);
    };

    var nameToParam = function(name){
        return name.replace(/ /g, "");
    }

    this.getParams = function () {
        var values = {},
            len = Parameters.length;
        for (var i = 0; i < len; i += 1) {
            var param = Parameters[i];
            values[nameToParam(param.name)] = parseFloat(param.node.value);
        }

        // hack
        var param = false;
        var generators = document.getElementById("generators").generator;
        for(var i = 0; i < generators.length; i++){
            if (generators[i].checked)
                param = generators[i].value;
        }

        values.generator = audio.generators[param]
        if(values.generator === undefined)
            values.generator = audio.generators.square;
        return values;
    };

    this.setParams = function (params) {
        var len = Parameters.length;
        for (var e in params){
            for (var i = 0; i < len; i += 1){
                var param = Parameters[i];
                if( nameToParam(param.name) === e ){
                    param.node.value = params[e];
                    break;
                }
            }
        }
    }

    this.randomize = function(){
        var len = Parameters.length;
        for (var i = 1; i < len; i++) {
            var param = Parameters[i];
            if( param.group === -1 ) continue;
            param.node.value = param.min + (param.max - param.min) * Math.random();
        }

        // hack
        var generators = document.getElementById("generators").generator;
        var i = (Math.random() * generators.length) | 0;
        generators[i].checked = true;

        this.play();
    }

    this.reset = function () {
        var len = Parameters.length;
        for (var i = 0; i < len; i += 1) {
            var param = Parameters[i];
            param.node.value = param.def;
        }
    }

    this.play = function(){
        var params = this.getParams();
        logreset();
        var data = this.generate(params);
        log('generate');
        var wave = audio.make(data);
        log('make wave');
        wave.play();
        return wave;
    }

}).apply(jsfx);
<commit_msg>phaser<commit_after>var jsfx = {};
(function () {
    var Parameters = [];

    this.loadParameters = function () {
        var grp = 0;
        var ap = function (name, min, max, def) {
            var param = { name: name,
                          min: min, max: max, def: def,
                          node: undefined, group: grp};
            Parameters.push(param);
        };

        ap("Master Volume",  0, 1, 0.3);
        grp++;

        ap("Attack Time",    0, 1, 0.1); // seconds
        ap("Sustain Time",   0, 2, 2); // seconds
        ap("Sustain Punch",  0, 3, 2);
        ap("Decay Time",     0, 2, 1); // seconds
        grp++;

        ap("Min Frequency",   20, 2400, 0);
        ap("Start Frequency", 20, 2400, 440);
        ap("Max Frequency",   20, 2400, 2000);
        ap("Slide",           -1, 1, 0);
        ap("Delta Slide",     -1, 1, 0);

        grp++;
        ap("Vibrato Depth",     0, 1, 0);
        ap("Vibrato Frequency", 0.01, 48, 8);
        ap("Vibrato Depth Slide",   -0.3, 1, 0);
        ap("Vibrato Frequency Slide", -1, 1, 0);

        grp++;
        ap("Change Amount", -12, 12, 0);
        ap("Change Speed",  0, 2, 0.1);

        grp++;
        ap("Square Duty", 0, 100, 50);
        ap("Duty Sweep",  0, 100, 50);

        grp++;
        ap("Repeat Speed", 0, 100, 50);

        grp++;
        ap("Phaser Offset", -1, 1, 0);
        ap("Phaser Sweep", -1, 1, 0);

        grp++;
        ap("LP Filter Cutoff", 0, 100, 50);
        ap("LP Filter Cutoff Sweep", 0, 100, 50);
        ap("LP Filter Resonance",    0, 100, 50);
        ap("HP Filter Cutoff",       0, 100, 50);
        ap("HP Filter Cutoff Sweep", 0, 100, 50);
    };

    this.generate = function(params){
        // useful consts
        var TAU = 2 * Math.PI;
        //var SampleRate = audio.SampleRate * 4;
        var SampleRate = audio.SampleRate;

        // useful functions
        var sin = Math.sin;

        // enveloping initialization
        var _ss = 1.0 + params.SustainPunch;
        var envelopes = [ {from: 0.0, to: 1.0, time: params.AttackTime},
                          {from: _ss, to: 1.0, time: params.SustainTime},
                          {from: 1.0, to: 0.0, time: params.DecayTime}];
        var envelopes_len = envelopes.length;

        // envelope sample calculation
        for(var i = 0; i < envelopes_len; i++){
            envelopes[i].samples = (envelopes[i].time * SampleRate) | 0
        }

        // envelope loop variables
        var envelope = undefined;
        var envelope_cur = 0.0;
        var envelope_idx = -1;
        var envelope_increment = 0.0;
        var envelope_last = -1;

        // count total samples
        var totalSamples = 0;
        for(var i = 0; i < envelopes_len; i++){
            totalSamples += envelopes[i].samples;
        }

        // out data samples
        var out = new Array(totalSamples);
        var sample = 0;

        // main generator
        var generator = params.generator;
        var generator_A = 0;
        var generator_B = 0;

        // phase calculation
        var phase = 0;
        var phase_speed = params.StartFrequency * TAU / SampleRate;

        // frequency limiter
        if(params.MinFrequency > params.StartFrequency)
            params.MinFrequency = params.StartFrequency;

        if(params.MaxFrequency < params.StartFrequency)
            params.MaxFrequency = params.StartFrequency;

        var phase_min_speed = params.MinFrequency * TAU / SampleRate;
        var phase_max_speed = params.MaxFrequency * TAU / SampleRate;

        // frequency vibrato
        var vibrato_phase = 0;
        var vibrato_phase_speed = params.VibratoFrequency * TAU / SampleRate;
        var vibrato_amplitude = params.VibratoDepth;

        // frequency vibrato slide
        var vibrato_phase_slide = 1.0 + Math.pow(params.VibratoFrequencySlide, 3.0) * 3.0 / SampleRate;
        var vibrato_amplitude_slide = params.VibratoDepthSlide / SampleRate;

        // slide calculation
        var slide = 1.0 + Math.pow(params.Slide, 3.0) * 3.0 / SampleRate;
        var delta_slide = Math.pow(params.DeltaSlide, 3.0) / (SampleRate * 1000);

        // arpeggiator
        var arpeggiator_time = 0;
        var arpeggiator_limit = params.ChangeSpeed * SampleRate;
        var arpeggiator_mod   = 1 + (params.ChangeAmount | 0) / 12.0;

        // phaser
        var phaser_max = 1024;
        var phaser_mask = 1023;
        var phaser_buffer = new Array(phaser_max);
        for(var _i = 0; _i < phaser_max; _i++)
            phaser_buffer[_i] = 0;
        var phaser_pos = 0;
        var phaser_offset = Math.pow(params.PhaserOffset, 2.0) * (phaser_max - 3);
        var phaser_offset_slide = Math.pow(params.PhaserSweep, 3.0) * 4000 / SampleRate;
        var phaser_enabled = (Math.abs(phaser_offset_slide) > 0.00001) ||
                             (Math.abs(phaser_offset) > 0.00001);
        console.debug(phaser_offset);
        console.debug(phaser_offset_slide);
        console.debug(phaser_enabled);

        // master volume controller
        var master_volume = params.MasterVolume;

        for(var i = 0; i < totalSamples; i++){
            // main generator
            sample = generator(phase, generator_A, generator_B);

            // phase calculation
            phase += phase_speed;

            // phase slide calculation
            slide += delta_slide;
            phase_speed *= slide;

            // arpeggiator
            if ( arpeggiator_time > arpeggiator_limit ){
                phase_speed *= arpeggiator_mod;
                arpeggiator_time = 0;
                // arpeggiator_limit = totalSamples * 2;
            }
            arpeggiator_time += 1;

            // frequency limiter
            if (phase_speed > phase_max_speed){
                phase_speed = phase_max_speed;
            } else if(phase_speed < phase_min_speed){
                phase_speed = phase_min_speed;
            }

            // frequency vibrato
            vibrato_phase += vibrato_phase_speed;
            var _vibrato_phase_mod = phase_speed * sin(vibrato_phase) * vibrato_amplitude;
            phase += _vibrato_phase_mod;

            // frequency vibrato slide
            vibrato_phase_speed *= vibrato_phase_slide;
            if(vibrato_amplitude_slide){
                vibrato_amplitude += vibrato_amplitude_slide;
                if(vibrato_amplitude < 0){
                    vibrato_amplitude = 0;
                    vibrato_amplitude_slide = 0;
                } else if (vibrato_amplitude > 1){
                    vibrato_amplitude = 1;
                    vibrato_amplitude_slide = 0;
                }
            }

            // phaser
            if (phaser_enabled) {
                phaser_offset += phaser_offset_slide;
                if( phaser_offset < 0){
                    phaser_offset = -phaser_offset;
                    phaser_offset_slide = -phaser_offset_slide;
                }
                if( phaser_offset > phaser_mask){
                    phaser_offset = phaser_mask;
                    phaser_offset_slide = 0;
                }

                phaser_buffer[phaser_pos] = sample;
                // phaser sample modification
                var _p = (phaser_pos - (phaser_offset|0) + phaser_max) & phaser_mask;
                sample += phaser_buffer[_p];
                phaser_pos = (phaser_pos + 1) & phaser_mask;
            }

            // envelope processing
            if( i > envelope_last ){
                envelope_idx += 1;
                if(envelope_idx < envelopes_len) // fault protection
                    envelope = envelopes[envelope_idx];
                envelope_cur = envelope.from;
                envelope_increment = (envelope.to - envelope.from) / envelope.samples;
                envelope_last += envelope.samples;
            }
            sample *= envelope_cur;
            envelope_cur += envelope_increment;

            // master volume controller
            sample *= master_volume;

            // prepare for next sample
            out[i] = sample;
        }

        /*var tn = (totalSamples/4)|0;
        var rout = new Array(tn);
        for(var i = 0 ; i < tn; i++){
            rout[i] = (out[i*4] + out[i*4 + 1] + out[i*4 + 2] + out[i*4 + 3]) / 4;
        }*/
        return out;
    }

    this.createConfigurationPanel = function (id) {
        this.loadParameters();
        var frag = document.createDocumentFragment(),
            len = Parameters.length,
            lastgroup = undefined;
        var group;
        for (var i = 0; i < len; i += 1) {
            var param = Parameters[i],
                row = document.createElement("tr"),
                fld = document.createElement("td"),
                input = document.createElement("input");
            if (param.group !== lastgroup) {
                group = document.createElement("table");
                frag.appendChild(group);
                lastgroup = param.group;
            }
            // add caption
            fld.appendChild(document.createTextNode(param.name));
            row.appendChild(fld);
            // create sliders
            fld = document.createElement("td");
            input.type = "range";
            input.step = (param.max - param.min) / 1000.0;
            input.min  = param.min;
            input.max  = param.max;
            input.value = param.def;
            param.node = input;
            fld.appendChild(input);
            row.appendChild(fld);
            group.appendChild(row);
        }
        var confpanel = document.getElementById(id);
        confpanel.appendChild(frag);
    };

    var nameToParam = function(name){
        return name.replace(/ /g, "");
    }

    this.getParams = function () {
        var values = {},
            len = Parameters.length;
        for (var i = 0; i < len; i += 1) {
            var param = Parameters[i];
            values[nameToParam(param.name)] = parseFloat(param.node.value);
        }

        // hack
        var param = false;
        var generators = document.getElementById("generators").generator;
        for(var i = 0; i < generators.length; i++){
            if (generators[i].checked)
                param = generators[i].value;
        }

        values.generator = audio.generators[param]
        if(values.generator === undefined)
            values.generator = audio.generators.square;
        return values;
    };

    this.setParams = function (params) {
        var len = Parameters.length;
        for (var e in params){
            for (var i = 0; i < len; i += 1){
                var param = Parameters[i];
                if( nameToParam(param.name) === e ){
                    param.node.value = params[e];
                    break;
                }
            }
        }
    }

    this.randomize = function(){
        var len = Parameters.length;
        for (var i = 1; i < len; i++) {
            var param = Parameters[i];
            if( param.group === -1 ) continue;
            param.node.value = param.min + (param.max - param.min) * Math.random();
        }

        // hack
        var generators = document.getElementById("generators").generator;
        var i = (Math.random() * generators.length) | 0;
        generators[i].checked = true;

        this.play();
    }

    this.reset = function () {
        var len = Parameters.length;
        for (var i = 0; i < len; i += 1) {
            var param = Parameters[i];
            param.node.value = param.def;
        }
    }

    this.play = function(){
        var params = this.getParams();
        logreset();
        var data = this.generate(params);
        log('generate');
        var wave = audio.make(data);
        log('make wave');
        wave.play();
        return wave;
    }

}).apply(jsfx);
<|endoftext|>


<commit_before>// Dean Attali 2015

var main = {

  bigImgEl : null,
  numImgs : null,

  init : function() {
    // Shorten the navbar after scrolling a little bit down
    $(window).scroll(function() {
        if ($(".navbar").offset().top > 50) {
            $(".navbar").addClass("top-nav-short");
        } else {
            $(".navbar").removeClass("top-nav-short");
        }
    });

    // On mobile, hide the avatar when expanding the navbar menu
    $('#main-navbar').on('show.bs.collapse', function () {
      $(".navbar").addClass("top-nav-expanded");
    })
    $('#main-navbar').on('hidden.bs.collapse', function () {
      $(".navbar").removeClass("top-nav-expanded");
    })

    // show a message if there is one to show
    qs = main.getQueryParams();
    if (qs.message) {
      $(".container")[0].innerHTML =
        '

' +
          '
' +
            qs.message +
        "

" +
        $(".container")[0].innerHTML;
    }

    // show the big header image
    main.initImgs();
  },

  initImgs : function() {
    // If the page was large images to randomly select from, choose an image
    if ($("#header-big-imgs").length > 0) {
      main.bigImgEl = $("#header-big-imgs");
      main.numImgs = main.bigImgEl.attr("data-num-img");

          // 2fc73a3a967e97599c9763d05e564189
	  // set an initial image
	  var imgInfo = main.getImgInfo();
	  var src = imgInfo.src;
	  var desc = imgInfo.desc;
  	  main.setImg(src, desc);

	  // For better UX, prefetch the next image so that it will already be loaded when we want to show it
  	  var getNextImg = function() {
	    var imgInfo = main.getImgInfo();
	    var src = imgInfo.src;
	    var desc = imgInfo.desc;

		var prefetchImg = new Image();
  		prefetchImg.src = src;
		// if I want to do something once the image is ready: `prefetchImg.onload = function(){}`

  		setTimeout(function(){
                  var img = $("

").addClass("big-img-transition").css("background-image", 'url(' + src + ')');
  		  $(".intro-header.big-img").prepend(img);
  		  setTimeout(function(){ img.css("opacity", "1"); }, 50);

		  // after the animation of fading in the new image is done, prefetch the next one
  		  //img.one("transitioned webkitTransitionEnd oTransitionEnd MSTransitionEnd", function(){
		  setTimeout(function() {
		    main.setImg(src, desc);
			img.remove();
  			getNextImg();
		  }, 1000);
  		  //});
  		}, 6000);
  	  };

	  // If there are multiple images, cycle through them
	  if (main.numImgs > 1) {
  	    getNextImg();
	  }
    }
  },

  getImgInfo : function() {
  	var randNum = Math.floor((Math.random() * main.numImgs) + 1);
    var src = main.bigImgEl.attr("data-img-src-" + randNum);
	var desc = main.bigImgEl.attr("data-img-desc-" + randNum);

	return {
	  src : src,
	  desc : desc
	}
  },

  setImg : function(src, desc) {
	$(".intro-header.big-img").css("background-image", 'url(' + src + ')');
	if (typeof desc !== typeof undefined && desc !== false) {
	  $(".img-desc").text(desc).show();
	} else {
	  $(".img-desc").hide();
	}
  },

 // get the GET parameters in the URL
 getQueryParams : function() {
    qs = document.location.search.split("+").join(" ");

    var params = {}, tokens,
        re = /[?&]?([^=]+)=([^&]*)/g;

    while (tokens = re.exec(qs)) {
        params[decodeURIComponent(tokens[1])]
            = decodeURIComponent(tokens[2]);
    }

    return params;
  }
};

document.addEventListener('DOMContentLoaded', main.init);
<commit_msg>update javascript to deal with URLs where query string contains multiple ?<commit_after>// Dean Attali 2015

var main = {

  bigImgEl : null,
  numImgs : null,

  init : function() {
    // Shorten the navbar after scrolling a little bit down
    $(window).scroll(function() {
        if ($(".navbar").offset().top > 50) {
            $(".navbar").addClass("top-nav-short");
        } else {
            $(".navbar").removeClass("top-nav-short");
        }
    });

    // On mobile, hide the avatar when expanding the navbar menu
    $('#main-navbar').on('show.bs.collapse', function () {
      $(".navbar").addClass("top-nav-expanded");
    })
    $('#main-navbar').on('hidden.bs.collapse', function () {
      $(".navbar").removeClass("top-nav-expanded");
    })

    // show a message if there is one to show
    var qs = main.getQueryParams();
    if (qs.message) {
      $(".container")[0].innerHTML =
        '

' +
          '
' +
            qs.message +
        "

" +
        $(".container")[0].innerHTML;
    }

    // show the big header image
    main.initImgs();
  },

  initImgs : function() {
    // If the page was large images to randomly select from, choose an image
    if ($("#header-big-imgs").length > 0) {
      main.bigImgEl = $("#header-big-imgs");
      main.numImgs = main.bigImgEl.attr("data-num-img");

          // 2fc73a3a967e97599c9763d05e564189
	  // set an initial image
	  var imgInfo = main.getImgInfo();
	  var src = imgInfo.src;
	  var desc = imgInfo.desc;
  	  main.setImg(src, desc);

	  // For better UX, prefetch the next image so that it will already be loaded when we want to show it
  	  var getNextImg = function() {
	    var imgInfo = main.getImgInfo();
	    var src = imgInfo.src;
	    var desc = imgInfo.desc;

		var prefetchImg = new Image();
  		prefetchImg.src = src;
		// if I want to do something once the image is ready: `prefetchImg.onload = function(){}`

  		setTimeout(function(){
                  var img = $("

").addClass("big-img-transition").css("background-image", 'url(' + src + ')');
  		  $(".intro-header.big-img").prepend(img);
  		  setTimeout(function(){ img.css("opacity", "1"); }, 50);

		  // after the animation of fading in the new image is done, prefetch the next one
  		  //img.one("transitioned webkitTransitionEnd oTransitionEnd MSTransitionEnd", function(){
		  setTimeout(function() {
		    main.setImg(src, desc);
			img.remove();
  			getNextImg();
		  }, 1000);
  		  //});
  		}, 6000);
  	  };

	  // If there are multiple images, cycle through them
	  if (main.numImgs > 1) {
  	    getNextImg();
	  }
    }
  },

  getImgInfo : function() {
  	var randNum = Math.floor((Math.random() * main.numImgs) + 1);
    var src = main.bigImgEl.attr("data-img-src-" + randNum);
	var desc = main.bigImgEl.attr("data-img-desc-" + randNum);

	return {
	  src : src,
	  desc : desc
	}
  },

  setImg : function(src, desc) {
	$(".intro-header.big-img").css("background-image", 'url(' + src + ')');
	if (typeof desc !== typeof undefined && desc !== false) {
	  $(".img-desc").text(desc).show();
	} else {
	  $(".img-desc").hide();
	}
  },

 // get the GET parameters in the URL
 getQueryParams : function() {
    var qs = document.location.search.replace(/\?/g, "&").split("+").join(" ");

    var params = {}, tokens,
        re = /[?&]?([^=]+)=([^&]*)/g;

    while (tokens = re.exec(qs)) {
        params[decodeURIComponent(tokens[1])]
            = decodeURIComponent(tokens[2]);
    }

    return params;
  }
};

document.addEventListener('DOMContentLoaded', main.init);
<|endoftext|>


<commit_before>function register() {
  window.location = 'https://api.twitch.tv/kraken/oauth2/authorize?response_type=token&client_id=cx94gjhz4ful945h0a3b4hbntiscia&redirect_uri=http://nico.kuro.ml&scope=chat_login';
}

function check() {
  var parser = document.createElement('a');
  parser.href = window.location.href;

  if (parser.hash.indexOf('#access_token=') > -1 && parser.hash.indexOf('&scope=chat_login') > -1) {
    //document.getElementById('url').style.visibility = 'visible';
    generate();
  } else {
    //document.getElementById('url').style.visibility = 'hidden';
  }
}

function generate() {
  //var chanName = document.getElementById('chanID');
  //var chanName = 'creditsToKuromachii'
  var parser = document.createElement('a');
  parser.href = window.location.href;
  var hash = parser.hash.replace('#access_token=','')
  hash = hash.replace('&scope=chat_login','')

  window.location = 'http://nico.kuro.ml/chat?token='+hash;//+'&chan_name='+chanName;
}

function load() {
  var parser = document.createElement('a');
  parser.href = window.location.href;
  var token = parser.search.split("token=")[1];
  //var token = parser.search.split("&chan_name=")[0].split("token=")[1];
  //var channelName = parser.search.split('&chan_name=').pop();

  //console.log(token);
  chat(token);
}

function chat(token) {
    var oauth = token;

    // You have to run .setup() before anything besides .listen()
    TAPIC.setup(oauth, function (username) {

      // By default, TAPIC refreshes API information every 5 seconds, which is the minimum.
      // Setting the refresh rate is optional.
      //TAPIC.setRefreshRate(10);

      // This is also required for a lot of things to work
       TAPIC.joinChannel(username, function () {
             tests();
         });
    });

    TAPIC.listen('message', function (e) {
      var output = '
'+'' +
        e.from +
        '' +
        (e.action ? '' : ':  ') +
        e.text +
        (e.action ? '' : '' )+'
';
        // e.emotes is the emotes, e.g. '25:0-4,12-16/1902:6-10'
        // https://github.com/justintv/Twitch-API/blob/master/IRC.md#privmsg
        // e.badges is an array of badges: https://discuss.dev.twitch.tv/t/beta-badge-api/6388
      writeChat( output );
    });
    TAPIC.listen('echoChat', function (e) {
      var output = '' + TAPIC.getDisplayName() +
        ' : ' + e;
      writeChat( output );
    });
}

// This is for the webpage's chat, it doesn't have anything directly to do with TAPIC.js
function writeChat(msg) {
  document.getElementById('chat').innerHTML += msg + '
';
  document.getElementById('chat').scrollTop = Number.MAX_SAFE_INTEGER;
}
<commit_msg>Little update<commit_after>function register() {
  window.location = 'https://api.twitch.tv/kraken/oauth2/authorize?response_type=token&client_id=cx94gjhz4ful945h0a3b4hbntiscia&redirect_uri=http://nico.kuro.ml&scope=chat_login';
}

function check() {
  var parser = document.createElement('a');
  parser.href = window.location.href;

  if (parser.hash.indexOf('#access_token=') > -1 && parser.hash.indexOf('&scope=chat_login') > -1) {
    //document.getElementById('url').style.visibility = 'visible';
    generate();
  } else {
    //document.getElementById('url').style.visibility = 'hidden';
  }
}

function generate() {
  //var chanName = document.getElementById('chanID');
  //var chanName = 'creditsToKuromachii'
  var parser = document.createElement('a');
  parser.href = window.location.href;
  var hash = parser.hash.replace('#access_token=','')
  hash = hash.replace('&scope=chat_login','')

  window.location = 'http://nico.kuro.ml/chat?token='+hash;//+'&chan_name='+chanName;
}

function load() {
  var parser = document.createElement('a');
  parser.href = window.location.href;
  var token = parser.search.split("token=")[1];
  //var token = parser.search.split("&chan_name=")[0].split("token=")[1];
  //var channelName = parser.search.split('&chan_name=').pop();

  //console.log(token);
  chat(token);
}

function chat(token) {
    var oauth = token;

    // You have to run .setup() before anything besides .listen()
    TAPIC.setup(oauth, function (username) {

      // By default, TAPIC refreshes API information every 5 seconds, which is the minimum.
      // Setting the refresh rate is optional.
      //TAPIC.setRefreshRate(10);

      // This is also required for a lot of things to work
       TAPIC.joinChannel(username, function () {
             tests();
         });
    });

    TAPIC.listen('message', function (e) {
      var output = '
'+'' +
        e.from +
        '' +
        (e.action ? '' : ':  ') +
        e.text +
        (e.action ? '' : '' )+'
';
        // e.emotes is the emotes, e.g. '25:0-4,12-16/1902:6-10'
        // https://github.com/justintv/Twitch-API/blob/master/IRC.md#privmsg
        // e.badges is an array of badges: https://discuss.dev.twitch.tv/t/beta-badge-api/6388
      writeChat( output );
    });
    // TAPIC.listen('echoChat', function (e) {
    //   var output = '' + TAPIC.getDisplayName() +
    //     ' : ' + e;
    //   writeChat( output );
    // });
}

// This is for the webpage's chat, it doesn't have anything directly to do with TAPIC.js
function writeChat(msg) {
  document.getElementById('chat').innerHTML += msg;
  document.getElementById('chat').scrollTop = Number.MAX_SAFE_INTEGER;
}
<|endoftext|>


<commit_before>
function FuzzyMesh(params) {
  const config = this.config = {
    useNormals: true,
    hairLength: 4,
    hairRadialSegments: 4,
    hairHeightSegments: 16,
    hairBase: 0.25,
    minFlex: 0.8,
    maxFlex: 1.2,
    fuzz: 0.5,
    gravity: 1.0,
    centrifugalForceFactor: 2.0,
    movementForceFactor: 0.75,
    movementDecay: 0.7,
    centrifugalDecay: 0.7,
    settleDecay: 0.97, // should always be higher than movementDecay and centrifugal decay
    ...params.config
  };

  const positions = params.model.vertices;
  const prefab = new THREE.ConeGeometry(
    config.hairBase,
    config.hairLength,
    config.hairRadialSegments,
    config.hairHeightSegments,
    true
  );

  prefab.translate(0, config.hairLength * 0.5, 0);

  const geometry = new BAS.PrefabBufferGeometry(prefab, positions.length);

  geometry.createAttribute('flex', 1, (data) => {
    data[0] = THREE.Math.randFloat(config.minFlex, config.maxFlex);
  });

  geometry.createAttribute('settleOffset', 1, (data) => {
    data[0] = THREE.Math.randFloat(0, Math.PI);
  });

  geometry.createAttribute('hairPosition', 3, (data, i) => {
    positions[i].toArray(data);
  });

  // bake normal based rotations into the position buffer
  const up = new THREE.Vector3(0, 1, 0);
  const normal = new THREE.Vector3();
  const position = new THREE.Vector3();
  const quat = new THREE.Quaternion();
  const positionBuffer = geometry.attributes.position.array;
  const baseDirectionBuffer = geometry.createAttribute('baseDirection', 3).array;

  let directions;

  if (config.useNormals === true) {
    const m = params.model;
    m.computeVertexNormals();

    directions = [];

    for (let i = 0; i < m.faces.length; i++) {
      const face = m.faces[i];

      directions[face.a] = face.vertexNormals[0];
      directions[face.b] = face.vertexNormals[1];
      directions[face.c] = face.vertexNormals[2];
    }
  }
  else {
    directions = positions;
  }

  for (let i = 0, offset = 0; i < positions.length; i++) {
    normal.copy(directions[i]);
    normal.x += THREE.Math.randFloatSpread(config.fuzz);
    normal.y += THREE.Math.randFloatSpread(config.fuzz);
    normal.z += THREE.Math.randFloatSpread(config.fuzz);
    normal.normalize();
    quat.setFromUnitVectors(up, normal);

    for (let j = 0; j < geometry.prefabVertexCount; j++, offset += 3) {
      position.copy(prefab.vertices[j]);
      position.applyQuaternion(quat);

      // positionBuffer[offset    ] = position.x;
      // positionBuffer[offset + 1] = position.y;
      // positionBuffer[offset + 2] = position.z;

      baseDirectionBuffer[offset    ] = normal.x;
      baseDirectionBuffer[offset + 1] = normal.y;
      baseDirectionBuffer[offset + 2] = normal.z;
    }
  }

  console.log(config.hairLength / config.hairHeightSegments)

  const material = new BAS.StandardAnimationMaterial({
    flatShading: true,
    // wireframe: true,
    uniformValues: {
      metalness: 0.5,
      roughness: 1.0
    },
    uniforms: {
      hairLength: {value: config.hairLength},
      heightStepSize: {value: config.hairLength / config.hairHeightSegments},
      settleTime: {value: 0.0},
      settleScale: {value: 1.0},
      globalForce: {value: new THREE.Vector3(0.0, -config.gravity, 0.0)},

      centrifugalForce: {value: 0.0},
      centrifugalAxis: {value: new THREE.Vector3()}
    },
    vertexParameters: `
      uniform float hairLength;
      uniform float heightStepSize;

      uniform vec3 globalForce;
      uniform float centrifugalForce;
      uniform vec3 centrifugalAxis;
      uniform float settleTime;
      uniform float settleScale;

      attribute float flex;
      attribute float settleOffset;
      attribute vec3 hairPosition;
      attribute vec3 baseDirection;

      vec3 UP = vec3(0.0, 1.0, 0.0);
    `,
    vertexFunctions: [
      BAS.ShaderChunk.quaternion_rotation,
      `
      // based on THREE.Quaternion.setFromUnitVectors
      // would be great if we can get rid of the conditionals
      vec4 quatFromUnitVectors(vec3 from, vec3 to) {
        vec3 v;
        float r = dot(from, to) + 1.0;

        if (r < 0.00001) {
          r = 0.0;

          if (abs(from.x) > abs(from.z)) {
            v.x = -from.y;
            v.y = from.x;
            v.z = 0.0;
          }
          else {
            v.x = 0.0;
            v.y = -from.z;
            v.z = from.y;
          }
        }
        else {
          v = cross(from, to);
        }

        return normalize(vec4(v.xyz, r));
      }
      `
    ],
    vertexPosition: `
      float l = position.y;
      float frc = l / hairLength;

      // frc = (frc) * 0.5;

      vec3 totalForce = globalForce * flex;

      totalForce *= 0.95 + 0.05 - (sin(settleTime + settleOffset) * 0.05 * settleScale);
      totalForce += baseDirection * centrifugalAxis * centrifugalForce;

      vec3 from = baseDirection;
      vec3 to = normalize(baseDirection + totalForce * frc);

      vec4 quat = quatFromUnitVectors(UP, to);
      transformed = rotateVector(quat, transformed);

      transformed += hairPosition;
    `
  });

  // might be possible to do smooth shading
  // geometry.computeVertexNormals();

  THREE.Mesh.call(this, geometry, material);
  this.frustumCulled = false;

  const base = new THREE.Mesh(
    params.model,
    new THREE.MeshStandardMaterial({
      roughness: 1.0,
      wireframe: true,
      // flatShading: true,
    })
  );
  this.add(base);

  // rotation stuff
  this.conjugate = new THREE.Quaternion();
  this.rotationAxis = new THREE.Vector3();
  this._angle = 0.0;
  this.previousAngle = this.angle;
  this.setRotationAxis('y');

  // position stuff
  this.previousPosition = this.position.clone();
  this.positionDelta = new THREE.Vector3();
  this.movementVelocity = new THREE.Vector3();
}

FuzzyMesh.prototype = Object.create(THREE.Mesh.prototype);
FuzzyMesh.prototype.constructor = FuzzyMesh;

Object.defineProperty(FuzzyMesh.prototype, 'pos', {
  get() {
    return this.position;
  },
  set(v) {
    this.previousPosition.copy(this.position);
    this.position.copy(v);
  }
});

Object.defineProperty(FuzzyMesh.prototype, 'angle', {
  get() {
    return this._angle;
  },
  set(v) {
    this.previousAngle = this._angle;
    this._angle = v;
  }
});

// todo: figure out which math we need to calculate centrifugal axis from rotation axis
FuzzyMesh.prototype.setRotationAxis = function(axis) {
  switch(axis) {
    case 'x':
      this.rotationAxis.set(1, 0, 0);
      this.material.uniforms.centrifugalAxis.value.set(0, 1, 1);
      break;
    case 'y':
      this.rotationAxis.set(0, 1, 0);
      this.material.uniforms.centrifugalAxis.value.set(1, 0, 1);
      break;
    case 'z':
      this.rotationAxis.set(0, 0, 1);
      this.material.uniforms.centrifugalAxis.value.set(1, 1, 0);
      break;
  }
};

FuzzyMesh.prototype.update = function() {
  // apply movement force
  this.positionDelta.copy(this.previousPosition).sub(this.position);

  this.movementVelocity.multiplyScalar(this.config.movementDecay);
  this.movementVelocity.x += this.positionDelta.x * this.config.movementForceFactor;
  this.movementVelocity.y += this.positionDelta.y * this.config.movementForceFactor;
  this.movementVelocity.z += this.positionDelta.z * this.config.movementForceFactor;

  this.material.uniforms.globalForce.value.set(
    this.movementVelocity.x,
    this.movementVelocity.y - this.config.gravity,
    this.movementVelocity.z
  );

  this.previousPosition.copy(this.position);

  // apply centrifugal force
  const rotationSpeed = Math.abs(this.previousAngle - this.angle) % (Math.PI * 2);
  this.material.uniforms.centrifugalForce.value *= this.config.centrifugalDecay;
  this.material.uniforms.centrifugalForce.value += rotationSpeed * this.config.centrifugalForceFactor;

  this.previousAngle = this.angle;

  // adjust global force based on rotation
  this.conjugate.copy(this.quaternion).conjugate();
  this.material.uniforms.globalForce.value.applyQuaternion(this.conjugate);

  // apply rotation to object
  this.quaternion.setFromAxisAngle(this.rotationAxis, this.angle);

  // rest / settle values
  this.material.uniforms.settleTime.value += (1/10);
  this.material.uniforms.settleScale.value *= this.config.settleDecay;
  this.material.uniforms.settleScale.value += (this.movementVelocity.length() + rotationSpeed) * 0.1;
  this.material.uniforms.settleScale.value > 1.0 && (this.material.uniforms.settleScale.value = 1.0);
  // console.log(this.movementVelocity.length());
};

const colors = {
  turquoise: 0x47debd,
  darkPurple: 0x2e044e,
  purple: 0x7821ec,
  yellow: 0xfff95d,
  white: 0xffffff,
  black: 0x000000
};

const root = new THREERoot({
  createCameraControls: true,
  zNear: 0.01,
  zFar: 1000
});

root.renderer.shadowMap.enabled = true;
root.renderer.setClearColor(colors.darkPurple);
root.camera.position.set(-10, 0, 20);
// root.controls.target.set(0, 3, 0);
// root.controls.autoRotate = true;

const light = new THREE.DirectionalLight(colors.turquoise);
light.position.set(0.125, 1, 0);
root.add(light);

const light2 = new THREE.DirectionalLight(colors.yellow);
light2.position.set(-0.125, -1, 0);
root.add(light2);

root.add(new THREE.AmbientLight(colors.purple));

new THREE.JSONLoader().load('https://s3-us-west-2.amazonaws.com/s.cdpn.io/304639/plus.json', (model) => {
  // test shapes, try different ones :D
  model = new THREE.SphereGeometry(4, 16, 16);
  // model = new THREE.PlaneGeometry(16, 16, 2, 2);
  // model = new THREE.CylinderGeometry(2, 2, 8, 128, 64, true);
  // model = new THREE.TorusGeometry(8, 1, 128, 512);
  // model = new THREE.TorusKnotGeometry(2, 0.1, 64, 64, 3, 5);

  // model.rotateX(Math.PI * -0.4);

  // const g = new THREE.SphereGeometry(2, 12, 12);
  // const m = new THREE.MeshBasicMaterial({
  //   color: 0xff0000,
  //   wireframe: true,
  // });
  //
  // model.vertices.forEach((v) => {
  //   const obj = new THREE.Mesh(g, m);
  //   obj.position.copy(v);
  //   root.add(obj);
  // });

  const fuzzy = new FuzzyMesh({
    model: model,
    config: {
      // hairBase: 0.05,
      // hairLength: 2.0,
      // hairHeightSegments: 2,
      // fuzz: 0.5,
    }
  });
  root.add(fuzzy);
  root.addUpdateCallback(() => {
    fuzzy.update();
  });

  // const axes = ['x', 'y', 'z'];
  // const p = new THREE.Vector3();
  // const tl = new TimelineMax({
  //   repeat: -1,
  //   delay: 1,
  //   repeatDelay: 0.5,
  //   onRepeat: () => {
  //     fuzzy.angle = 0;
  //   fuzzy.setRotationAxis(axes[Math.random() * 3 | 0]);
  //   },
  //   onUpdate: () => {
  //     fuzzy.pos = p;
  //   }
  // });
  //
  // tl.to(p, 0.5, {y: 8, ease: Power2.easeOut});
  // tl.to(p, 0.5, {y: 0, ease: Power2.easeIn});
  // tl.to(p, 0.1, {y: -2, ease: Power2.easeOut});
  // tl.to(p, 0.5, {y: 0, ease: Power2.easeOut});
  // tl.fromTo(fuzzy, 1.0, {angle: 0}, {angle: Math.PI * 2 * (Math.random() > 0.5 ? 1 : -1), ease: Power1.easeInOut}, 0);

  fuzzy.setRotationAxis('x');
  const position = new THREE.Vector3();
  const tl = new TimelineMax({repeat: -1, yoyo: true, repeatDelay: 0});
  // tl.fromTo(position, 2, {x: -6}, {x: 6, ease: Power3.easeInOut, onUpdate: () => {
  //   fuzzy.pos = position;
  // }}, 0);
  tl.fromTo(fuzzy, 8, {angle: Math.PI * -1}, {angle: Math.PI * 1, ease: Power1.easeInOut}, 0);

});
<commit_msg>more complicated version with recurve rotation calculation<commit_after>
function FuzzyMesh(params) {
  const config = this.config = {
    useNormals: true,
    hairLength: 4,
    hairRadialSegments: 4,
    hairHeightSegments: 16,
    hairBase: 0.05,
    minFlex: 0.8,
    maxFlex: 1.2,
    fuzz: 0.5,
    gravity: 2.0,
    centrifugalForceFactor: 2.0,
    movementForceFactor: 0.75,
    movementDecay: 0.7,
    centrifugalDecay: 0.7,
    settleDecay: 0.97, // should always be higher than movementDecay and centrifugal decay
    ...params.config
  };

  const positions = params.model.vertices;
  const prefab = new THREE.ConeGeometry(
    config.hairBase,
    config.hairLength,
    config.hairRadialSegments,
    config.hairHeightSegments,
    true
  );

  prefab.translate(0, config.hairLength * 0.5, 0);

  const geometry = new BAS.PrefabBufferGeometry(prefab, positions.length);

  geometry.createAttribute('flex', 1, (data) => {
    data[0] = THREE.Math.randFloat(config.minFlex, config.maxFlex);
  });

  geometry.createAttribute('settleOffset', 1, (data) => {
    data[0] = THREE.Math.randFloat(0, Math.PI);
  });

  geometry.createAttribute('hairPosition', 3, (data, i) => {
    positions[i].toArray(data);
  });

  // bake normal based rotations into the position buffer
  const baseDirectionBuffer = geometry.createAttribute('baseDirection', 3).array;

  let directions;

  if (config.useNormals === true) {
    const m = params.model;
    m.computeVertexNormals();

    directions = [];

    for (let i = 0; i < m.faces.length; i++) {
      const face = m.faces[i];

      directions[face.a] = face.vertexNormals[0];
      directions[face.b] = face.vertexNormals[1];
      directions[face.c] = face.vertexNormals[2];
    }
  }
  else {
    directions = positions;
  }

  const normal = new THREE.Vector3();

  for (let i = 0, offset = 0; i < positions.length; i++) {
    normal.copy(directions[i]);
    normal.x += THREE.Math.randFloatSpread(config.fuzz);
    normal.y += THREE.Math.randFloatSpread(config.fuzz);
    normal.z += THREE.Math.randFloatSpread(config.fuzz);
    normal.normalize();

    for (let j = 0; j < geometry.prefabVertexCount; j++, offset += 3) {
      baseDirectionBuffer[offset    ] = normal.x;
      baseDirectionBuffer[offset + 1] = normal.y;
      baseDirectionBuffer[offset + 2] = normal.z;
    }
  }

  const material = new BAS.StandardAnimationMaterial({
    flatShading: true,
    wireframe: false,
    uniformValues: {
      metalness: 0.5,
      roughness: 1.0
    },
    uniforms: {
      hairLength: {value: config.hairLength},
      settleTime: {value: 0.0},
      settleScale: {value: 1.0},
      globalForce: {value: new THREE.Vector3(0.0, -config.gravity, 0.0)},

      centrifugalForce: {value: 0.0},
      centrifugalAxis: {value: new THREE.Vector3()}
    },
    defines: {
      'HAIR_LENGTH': (config.hairLength).toFixed(2),
      'SEGMENT_STEP': (config.hairLength / config.hairHeightSegments).toFixed(2),
      'FORCE_STEP': (1.0 / config.hairLength).toFixed(2)
    },
    vertexParameters: `
      uniform float hairLength;
      uniform float heightSteps;
      uniform float heightStepSize;

      uniform vec3 globalForce;
      uniform float centrifugalForce;
      uniform vec3 centrifugalAxis;
      uniform float settleTime;
      uniform float settleScale;

      attribute float flex;
      attribute float settleOffset;
      attribute vec3 hairPosition;
      attribute vec3 baseDirection;

      vec3 UP = vec3(0.0, 1.0, 0.0);
    `,
    vertexFunctions: [
      BAS.ShaderChunk.quaternion_rotation,
      `
      // based on THREE.Quaternion.setFromUnitVectors
      // would be great if we can get rid of the conditionals
      vec4 quatFromUnitVectors(vec3 from, vec3 to) {
        vec3 v;
        float r = dot(from, to) + 1.0;

        if (r < 0.00001) {
          r = 0.0;

          if (abs(from.x) > abs(from.z)) {
            v.x = -from.y;
            v.y = from.x;
            v.z = 0.0;
          }
          else {
            v.x = 0.0;
            v.y = -from.z;
            v.z = from.y;
          }
        }
        else {
          v = cross(from, to);
        }

        return normalize(vec4(v.xyz, r));
      }
      `
    ],
    vertexPosition: `
      // float l = position.y;
      // float frc = l / hairLength;
      // vec3 totalForce = globalForce * flex;
      // totalForce *= 0.95 + 0.05 - (sin(settleTime + settleOffset) * 0.05 * settleScale);
      // totalForce += baseDirection * centrifugalAxis * centrifugalForce;
      // vec3 from = baseDirection;
      // vec3 to = normalize(baseDirection + totalForce * frc);
      // vec4 quat = quatFromUnitVectors(UP, to);
      // transformed = rotateVector(quat, transformed);

      vec3 totalForce = globalForce * flex;
      totalForce *= 0.95 + 0.05 - (sin(settleTime + settleOffset) * 0.05 * settleScale);
      totalForce += baseDirection * centrifugalAxis * centrifugalForce;

      vec3 result;

      vec3 to = normalize(baseDirection + totalForce * position.y / HAIR_LENGTH);
      vec4 q = quatFromUnitVectors(UP, to);
      vec3 v = vec3(position.x, 0.0, position.z);

      result += rotateVector(q, v);

      for (float i = 0.0; i < HAIR_LENGTH; i += SEGMENT_STEP) {
        if (position.y <= i) break;

        vec3 to = normalize(baseDirection + totalForce * i * FORCE_STEP);
        vec4 q = quatFromUnitVectors(UP, to);
        vec3 v = vec3(0.0, SEGMENT_STEP, 0.0);

        result += rotateVector(q, v);
      }

      transformed = result;
      transformed += hairPosition;
    `
  });

  // console.log(material.defines)

  THREE.Mesh.call(this, geometry, material);
  this.frustumCulled = false;

  const base = new THREE.Mesh(
    params.model,
    new THREE.MeshStandardMaterial({
      roughness: 1.0,
      // wireframe: true,
      // flatShading: true,
    })
  );
  this.add(base);

  // rotation stuff
  this.conjugate = new THREE.Quaternion();
  this.rotationAxis = new THREE.Vector3();
  this._angle = 0.0;
  this.previousAngle = this.angle;
  this.setRotationAxis('y');

  // position stuff
  this.previousPosition = this.position.clone();
  this.positionDelta = new THREE.Vector3();
  this.movementVelocity = new THREE.Vector3();
}

FuzzyMesh.prototype = Object.create(THREE.Mesh.prototype);
FuzzyMesh.prototype.constructor = FuzzyMesh;

Object.defineProperty(FuzzyMesh.prototype, 'pos', {
  get() {
    return this.position;
  },
  set(v) {
    this.previousPosition.copy(this.position);
    this.position.copy(v);
  }
});

Object.defineProperty(FuzzyMesh.prototype, 'angle', {
  get() {
    return this._angle;
  },
  set(v) {
    this.previousAngle = this._angle;
    this._angle = v;
  }
});

// todo: figure out which math we need to calculate centrifugal axis from rotation axis
FuzzyMesh.prototype.setRotationAxis = function(axis) {
  switch(axis) {
    case 'x':
      this.rotationAxis.set(1, 0, 0);
      this.material.uniforms.centrifugalAxis.value.set(0, 1, 1);
      break;
    case 'y':
      this.rotationAxis.set(0, 1, 0);
      this.material.uniforms.centrifugalAxis.value.set(1, 0, 1);
      break;
    case 'z':
      this.rotationAxis.set(0, 0, 1);
      this.material.uniforms.centrifugalAxis.value.set(1, 1, 0);
      break;
  }
};

FuzzyMesh.prototype.update = function() {
  // apply movement force
  this.positionDelta.copy(this.previousPosition).sub(this.position);

  this.movementVelocity.multiplyScalar(this.config.movementDecay);
  this.movementVelocity.x += this.positionDelta.x * this.config.movementForceFactor;
  this.movementVelocity.y += this.positionDelta.y * this.config.movementForceFactor;
  this.movementVelocity.z += this.positionDelta.z * this.config.movementForceFactor;

  this.material.uniforms.globalForce.value.set(
    this.movementVelocity.x,
    this.movementVelocity.y - this.config.gravity,
    this.movementVelocity.z
  );

  this.previousPosition.copy(this.position);

  // apply centrifugal force
  const rotationSpeed = Math.abs(this.previousAngle - this.angle) % (Math.PI * 2);
  this.material.uniforms.centrifugalForce.value *= this.config.centrifugalDecay;
  this.material.uniforms.centrifugalForce.value += rotationSpeed * this.config.centrifugalForceFactor;

  this.previousAngle = this.angle;

  // adjust global force based on rotation
  this.conjugate.copy(this.quaternion).conjugate();
  this.material.uniforms.globalForce.value.applyQuaternion(this.conjugate);

  // apply rotation to object
  this.quaternion.setFromAxisAngle(this.rotationAxis, this.angle);

  // rest / settle values
  this.material.uniforms.settleTime.value += (1/10);
  this.material.uniforms.settleScale.value *= this.config.settleDecay;
  this.material.uniforms.settleScale.value += (this.movementVelocity.length() + rotationSpeed) * 0.1;
  this.material.uniforms.settleScale.value > 1.0 && (this.material.uniforms.settleScale.value = 1.0);
  // console.log(this.movementVelocity.length());
};

const colors = {
  turquoise: 0x47debd,
  darkPurple: 0x2e044e,
  purple: 0x7821ec,
  yellow: 0xfff95d,
  white: 0xffffff,
  black: 0x000000
};

const root = new THREERoot({
  createCameraControls: true,
  zNear: 0.01,
  zFar: 1000
});

root.renderer.shadowMap.enabled = true;
root.renderer.setClearColor(colors.darkPurple);
root.camera.position.set(-10, 0, 20);
// root.controls.target.set(0, 3, 0);
// root.controls.autoRotate = true;

const light = new THREE.DirectionalLight(colors.turquoise);
light.position.set(0.125, 1, 0);
root.add(light);

const light2 = new THREE.DirectionalLight(colors.yellow);
light2.position.set(-0.125, -1, 0);
root.add(light2);

root.add(new THREE.AmbientLight(colors.purple));

root.add(new THREE.AxisHelper(10));

new THREE.JSONLoader().load('https://s3-us-west-2.amazonaws.com/s.cdpn.io/304639/plus.json', (model) => {
  // test shapes, try different ones :D
  // model = new THREE.SphereGeometry(4, 256, 128);
  // model = new THREE.PlaneGeometry(16, 16, 2, 2);
  // model = new THREE.CylinderGeometry(2, 2, 8, 128, 64, true);
  model = new THREE.TorusGeometry(8, 1, 128, 512);
  // model = new THREE.TorusKnotGeometry(2, 0.1, 64, 64, 3, 5);

  // model.rotateX(Math.PI * -0.4);

  // const g = new THREE.SphereGeometry(1, 12, 12);
  // const m = new THREE.MeshBasicMaterial({
  //   color: 0xff0000,
  //   wireframe: true,
  // });
  //
  // model.vertices.forEach((v) => {
  //   const obj = new THREE.Mesh(g, m);
  //   obj.position.copy(v);
  //   root.add(obj);
  // });

  const fuzzy = new FuzzyMesh({
    model: model,
    config: {}
  });
  root.add(fuzzy);
  root.addUpdateCallback(() => {
    fuzzy.update();
  });

  const axes = ['x', 'y', 'z'];
  const p = new THREE.Vector3();
  const tl = new TimelineMax({
    repeat: -1,
    delay: 1,
    repeatDelay: 0.5,
    onRepeat: () => {
      fuzzy.angle = 0;
    fuzzy.setRotationAxis(axes[Math.random() * 3 | 0]);
    },
    onUpdate: () => {
      fuzzy.pos = p;
    }
  });

  tl.to(p, 0.5, {y: 16, ease: Power2.easeOut});
  tl.to(p, 0.5, {y: 0, ease: Power2.easeIn});
  tl.to(p, 0.1, {y: -4, ease: Power2.easeOut});
  tl.to(p, 0.5, {y: 0, ease: Power2.easeOut});
  tl.fromTo(fuzzy, 1.0, {angle: 0}, {angle: Math.PI * 2 * (Math.random() > 0.5 ? 1 : -1), ease: Power1.easeInOut}, 0);

  // fuzzy.setRotationAxis('x');
  // const position = new THREE.Vector3();
  // const tl = new TimelineMax({repeat: -1, yoyo: true, repeatDelay: 0});
  // tl.fromTo(position, 2, {x: -6}, {x: 6, ease: Power3.easeInOut, onUpdate: () => {
  //   fuzzy.pos = position;
  // }}, 0);
  // tl.fromTo(fuzzy, 8, {angle: Math.PI * -1}, {angle: Math.PI * 1, ease: Power1.easeInOut}, 0);

});
<|endoftext|>