flow: color, dialog, Evolved

This commit is contained in:
cha0s 2021-02-15 16:41:51 -06:00
parent d69f1418e8
commit 15c9851f2a
30 changed files with 17963 additions and 20 deletions

View File

@ -0,0 +1 @@
module.exports = require('../../config/.eslintrc');

7
packages/color/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
**/*.js
**/*.map
!/.*
!/postcss.config.js
!/webpack.config.js
!src/**/*.js
!/test/**/*.js

View File

@ -0,0 +1 @@
module.exports = require('../../config/.neutrinorc');

View File

@ -0,0 +1,46 @@
{
"name": "@avocado/color",
"version": "1.0.0",
"main": "index.js",
"author": "cha0s",
"license": "MIT",
"scripts": {
"build": "NODE_PATH=./node_modules webpack --mode production",
"clean": "rm -rf yarn.lock node_modules $(node -e \"process.stdout.write(require('./package.json').files.filter((file) => {const parts = file.split('/'); return 1 === parts.length || 'test' !== parts[0];}).join(' '));\") && yarn",
"forcepub": "npm unpublish --force $(node -e 'const {name, version} = require(`./package.json`); process.stdout.write(`${name}@${version}`)') && npm publish",
"lint": "NODE_PATH=./node_modules eslint --format codeframe --ext mjs,js .",
"test": "yarn --silent run build --display none && mocha --colors test.js"
},
"files": [
"index.js",
"index.js.map",
"test.js",
"test.js.map"
],
"dependencies": {
"@avocado/behavior": "^2.0.0",
"@avocado/traits": "^2.0.0",
"@latus/core": "^2.0.0",
"debug": "4.3.1",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2"
},
"devDependencies": {
"@neutrinojs/airbnb": "^9.4.0",
"@neutrinojs/banner": "^9.4.0",
"@neutrinojs/copy": "^9.4.0",
"@neutrinojs/mocha": "^9.4.0",
"@neutrinojs/react": "^9.4.0",
"autoprefixer": "^9.8.6",
"chai": "4.2.0",
"eslint": "^7",
"eslint-import-resolver-webpack": "0.13.0",
"glob": "7.1.6",
"mocha": "^8",
"neutrino": "^9.4.0",
"source-map-support": "0.5.19",
"webpack": "^4",
"webpack-cli": "^3",
"webpack-node-externals": "2.5.2"
}
}

View File

@ -0,0 +1,6 @@
/* eslint-disable global-require */
module.exports = {
plugins: [
require('autoprefixer'),
],
};

View File

@ -0,0 +1,27 @@
import get from 'lodash.get';
import set from 'lodash.set';
import hsv2rgb from './hsv2rgb';
export default () => ({
rainbow: ({
active,
traits,
i,
length,
}) => {
const [red, green, blue] = hsv2rgb([i / length, 1, 1]);
set(traits, 'Colorized.state', {red, green, blue});
if (active) {
const lfo = get(traits, 'Evolving.params.lfo', {});
lfo.hue = {
frequency: 1,
location: ((i % length) / length),
magnitude: 1,
modulators: ['Sawtooth'],
};
set(traits, 'Evolving.params.lfo', lfo);
}
return traits;
},
});

View File

@ -0,0 +1,23 @@
export default ([h, s, v]) => {
let r;
let g;
let b;
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
switch (i % 6) {
default: [r, g, b] = [v, t, p]; break;
case 1: [r, g, b] = [q, v, p]; break;
case 2: [r, g, b] = [p, v, t]; break;
case 3: [r, g, b] = [p, q, v]; break;
case 4: [r, g, b] = [t, p, v]; break;
case 5: [r, g, b] = [v, p, q]; break;
}
return [
Math.round(r * 255),
Math.round(g * 255),
Math.round(b * 255),
];
};

View File

@ -0,0 +1,12 @@
import {gatherWithLatus} from '@latus/core';
import dialogFunctions from './dialog-functions';
export default {
hooks: {
'@avocado/dialog/functions': dialogFunctions,
'@avocado/traits': gatherWithLatus(
require.context('./traits', false, /\.js$/),
),
},
};

View File

@ -0,0 +1,36 @@
export default ([r, g, b]) => {
const rabs = r / 255;
const gabs = g / 255;
const babs = b / 255;
const v = Math.max(rabs, gabs, babs);
const diff = v - Math.min(rabs, gabs, babs);
const diffc = (c) => (v - c) / 6 / diff + 1 / 2;
let h;
let s;
if (0 === diff) {
h = 0;
s = 0;
}
else {
s = diff / v;
const rr = diffc(rabs);
const gg = diffc(gabs);
const bb = diffc(babs);
if (rabs === v) {
h = bb - gg;
}
else if (gabs === v) {
h = (1 / 3) + rr - bb;
}
else if (babs === v) {
h = (2 / 3) + gg - rr;
}
if (h < 0) {
h += 1;
}
else if (h > 1) {
h -= 1;
}
}
return [h, s, v];
};

View File

@ -1,7 +1,8 @@
import {StateProperty, Trait} from '@avocado/traits'; import {StateProperty, Trait} from '@avocado/traits';
import {compose} from '@latus/core'; import {compose} from '@latus/core';
import Color from '../color'; import hsv2rgb from '../hsv2rgb';
import rgb2hsv from '../rgb2hsv';
const decorate = compose( const decorate = compose(
StateProperty('red', { StateProperty('red', {
@ -15,7 +16,7 @@ const decorate = compose(
}), }),
); );
export default () => class Colored extends decorate(Trait) { export default () => class Colorized extends decorate(Trait) {
static children() { static children() {
return { return {
@ -69,7 +70,7 @@ export default () => class Colored extends decorate(Trait) {
} }
get hue() { get hue() {
return Color.rgb2hsv(this.red, this.green, this.blue)[0]; return rgb2hsv([this.red, this.green, this.blue])[0];
} }
set hue(hue) { set hue(hue) {
@ -87,7 +88,7 @@ export default () => class Colored extends decorate(Trait) {
} }
get saturation() { get saturation() {
return Color.rgb2hsv(this.red, this.green, this.blue)[1]; return rgb2hsv([this.red, this.green, this.blue])[1];
} }
set saturation(saturation) { set saturation(saturation) {
@ -95,7 +96,7 @@ export default () => class Colored extends decorate(Trait) {
} }
get value() { get value() {
return Color.rgb2hsv(this.red, this.green, this.blue)[2]; return rgb2hsv([this.red, this.green, this.blue])[2];
} }
set value(value) { set value(value) {
@ -103,19 +104,19 @@ export default () => class Colored extends decorate(Trait) {
} }
wrapHsvSet(i, nv) { wrapHsvSet(i, nv) {
const hsv = Color.rgb2hsv(this.red, this.green, this.blue); const hsv = rgb2hsv([this.red, this.green, this.blue]);
const keys = ['hue', 'saturation', 'value']; const keys = ['hue', 'saturation', 'value'];
if (nv !== hsv[i]) { if (nv !== hsv[i]) {
hsv[i] = nv; hsv[i] = nv;
[this.red, this.green, this.blue] = Color.hsv2rgb(...hsv); [this.red, this.green, this.blue] = hsv2rgb(hsv);
this.entity.emit(`${keys[i]}Changed`); this.entity.emit(`${keys[i]}Changed`);
} }
} }
wrapRgbSet(fn) { wrapRgbSet(fn) {
const o = Color.rgb2hsv(this.red, this.green, this.blue); const o = rgb2hsv([this.red, this.green, this.blue]);
fn(); fn();
const n = Color.rgb2hsv(this.red, this.green, this.blue); const n = rgb2hsv([this.red, this.green, this.blue]);
['hue', 'saturation', 'value'].forEach((key, i) => { ['hue', 'saturation', 'value'].forEach((key, i) => {
if (n[i] !== o[i]) { if (n[i] !== o[i]) {
this.entity.emit(`${key}Changed`); this.entity.emit(`${key}Changed`);

View File

@ -0,0 +1,5 @@
import {expect} from 'chai';
it('exists', () => {
expect(true).to.be.true;
});

View File

@ -0,0 +1,3 @@
const neutrino = require('neutrino');
module.exports = neutrino(require(`${__dirname}/.neutrinorc`)).webpack();

8518
packages/color/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
module.exports = require('../../config/.eslintrc');

7
packages/dialog/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
**/*.js
**/*.map
!/.*
!/postcss.config.js
!/webpack.config.js
!src/**/*.js
!/test/**/*.js

View File

@ -0,0 +1 @@
module.exports = require('../../config/.neutrinorc');

View File

@ -0,0 +1,49 @@
{
"name": "@avocado/dialog",
"version": "1.0.0",
"main": "index.js",
"author": "cha0s",
"license": "MIT",
"scripts": {
"build": "NODE_PATH=./node_modules webpack --mode production",
"clean": "rm -rf yarn.lock node_modules $(node -e \"process.stdout.write(require('./package.json').files.filter((file) => {const parts = file.split('/'); return 1 === parts.length || 'test' !== parts[0];}).join(' '));\") && yarn",
"forcepub": "npm unpublish --force $(node -e 'const {name, version} = require(`./package.json`); process.stdout.write(`${name}@${version}`)') && npm publish",
"lint": "NODE_PATH=./node_modules eslint --format codeframe --ext mjs,js .",
"test": "yarn --silent run build --display none && mocha --colors test.js"
},
"files": [
"index.js",
"index.js.map",
"test.js",
"test.js.map"
],
"dependencies": {
"@avocado/traits": "^2.0.0",
"@latus/core": "^2.0.0",
"debug": "4.3.1",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"remark-mdx": "2.0.0-next.8",
"remark-parse": "8.0.2",
"unified": "^9.2.0",
"unist-util-visit-parents": "^3.1.1"
},
"devDependencies": {
"@neutrinojs/airbnb": "^9.4.0",
"@neutrinojs/banner": "^9.4.0",
"@neutrinojs/copy": "^9.4.0",
"@neutrinojs/mocha": "^9.4.0",
"@neutrinojs/react": "^9.4.0",
"autoprefixer": "^9.8.6",
"chai": "4.2.0",
"eslint": "^7",
"eslint-import-resolver-webpack": "0.13.0",
"glob": "7.1.6",
"mocha": "^8",
"neutrino": "^9.4.0",
"source-map-support": "0.5.19",
"webpack": "^4",
"webpack-cli": "^3",
"webpack-node-externals": "2.5.2"
}
}

View File

@ -0,0 +1,6 @@
/* eslint-disable global-require */
module.exports = {
plugins: [
require('autoprefixer'),
],
};

View File

@ -0,0 +1,56 @@
import get from 'lodash.get';
import set from 'lodash.set';
export default () => ({
rate: ({
frequency = 0.05,
traits,
}) => {
set(traits, 'DialogText.state.rate', parseInt(frequency, 10));
return traits;
},
shake: ({
frequency = 1,
magnitude = 3,
traits,
i,
length,
}) => {
const lfo = get(traits, 'Evolving.params.lfo', {});
lfo.x = {
frequency,
location: ((i % length) / length),
magnitude: magnitude / 2,
median: 0,
modulators: ['Random'],
};
lfo.y = {
frequency,
location: ((i % length) / length),
magnitude,
median: 0,
modulators: ['Random'],
};
set(traits, 'Evolving.params.lfo', lfo);
return traits;
},
wave: ({
frequency = 1,
magnitude = 3,
modulator = 'Sine',
traits,
i,
length,
}) => {
const lfo = get(traits, 'Evolving.params.lfo', {});
lfo.y = {
frequency,
location: ((i % length) / length),
magnitude,
median: 0,
modulators: [modulator],
};
set(traits, 'Evolving.params.lfo', lfo);
return traits;
},
});

View File

@ -0,0 +1,28 @@
import {gatherWithLatus} from '@latus/core';
import dialogFunctions from './dialog-functions';
import parse from './parser';
import wordize from './wordize';
export default {
hooks: {
'@avocado/dialog/functions': dialogFunctions,
'@avocado/traits': gatherWithLatus(
require.context('./traits', false, /\.js$/),
),
'@latus/core/starting': (latus) => {
const dialogFunctions = latus.invokeReduce('@avocado/dialog/functions');
const resolver = (type) => (dialogFunctions[type] ? dialogFunctions[type] : (i) => i);
latus.set('%dialogParser', async (text) => {
const {Entity} = latus.get('%resources');
const letters = await Promise.all(
parse(text, resolver).map((letter) => Entity.load({traits: letter})),
);
return {
letters,
words: await wordize(letters, latus),
};
});
},
},
};

View File

@ -0,0 +1,109 @@
import get from 'lodash.get';
import set from 'lodash.set';
import mdx from 'remark-mdx';
import parse from 'remark-parse';
import unified from 'unified';
import visit from 'unist-util-visit-parents';
const letterCount = (node) => {
if ('text' === node.type) {
return node.value.length;
}
return node.children.length > 0
? node.children.reduce((count, child) => count + letterCount(child), 0)
: 0;
};
const decorateWithLetters = (resolver) => (node, ancestors) => {
/* eslint-disable no-param-reassign */
if ('text' === node.type) {
const letters = node.value
.replace(/\s/g, '\u00A0')
.split('');
node.letters = letters.map((letter) => (
ancestors.reduce(
(traits, ancestor) => ancestor.traits(traits),
{
DomNode: {
params: {
style: {
display: 'inline-block',
position: 'relative',
},
},
},
DomText: {
params: {
centered: false,
},
},
Textual: {
state: {
text: letter,
},
},
Visible: {
state: {
opacity: 0,
},
},
},
)
));
return;
}
node.traits = (traits) => traits;
if ('emphasis' === node.type) {
node.traits = (traits) => {
const style = get(traits, 'DomNode.params.style', {});
style.fontStyle = 'italic';
set(traits, 'DomNode.params.style', style);
return traits;
};
}
if ('strong' === node.type) {
node.traits = (traits) => {
const style = get(traits, 'DomNode.params.style', {});
style.fontWeight = 'bold';
set(traits, 'DomNode.params.style', style);
return traits;
};
}
if (
'mdxBlockElement' === node.type
|| 'mdxSpanElement' === node.type
) {
const {attributes, name} = node;
const length = letterCount(node);
let i = 0;
node.traits = (traits) => {
const args = attributes.reduce((r, {name, value}) => ({
...r,
[name]: (value?.value ? value.value : value) || true,
}), {});
return resolver(name)({
length,
...args,
i: i++,
traits,
});
};
}
/* eslint-enable no-param-reassign */
};
const lettersFor = (node) => {
if (node.children) {
return node.children.reduce(
(r, child) => r.concat(...lettersFor(child)),
[],
);
}
return node.letters || [];
};
export default (source, resolver) => {
const tree = unified().use(parse).use(mdx).parse(source);
visit(tree, decorateWithLetters(resolver));
return lettersFor(tree);
};

View File

@ -0,0 +1,16 @@
import {StateProperty, Trait} from '@avocado/traits';
import {compose} from '@latus/core';
const decorate = compose(
StateProperty('rate'),
);
export default () => class DialogText extends decorate(Trait) {
static defaultState() {
return {
rate: 0.1,
};
}
};

View File

@ -0,0 +1,90 @@
import {Trait} from '@avocado/traits';
export default (latus) => class Dialog extends Trait {
#dialog;
#letters = [];
#nextLetter = 0;
#thisLetter = 0;
#words = [];
static defaultParams() {
return {
rate: 0.05,
};
}
static dependencies() {
return [
'Textual',
'DomNode',
];
}
// eslint-disable-next-line class-methods-use-this
hooks() {
return {
visibleAabbs: () => [-64, -64, 64, 64],
};
}
async load(json) {
await super.load(json);
if ('client' === process.env.SIDE) {
this.entity.node.className = 'dialog';
const {text} = this.entity;
if (text) {
({
letters: this.#letters,
words: this.#words,
} = await latus.get('%dialogParser')(
`<rate frequency={${this.params.rate}}>${text}</rate>`,
));
if (this.#letters.length > 0) {
this.showThisLetter();
this.#words.forEach((word) => {
// eslint-disable-next-line no-param-reassign
word.parentNode = this.entity.node;
});
}
}
}
}
showThisLetter() {
const letter = this.#letters[this.#thisLetter];
this.#nextLetter += letter.rate;
letter.opacity = 1;
}
skip() {
for (let i = 0; i < this.#letters.length; ++i) {
this.#letters[i].opacity = 1;
}
this.#nextLetter = Infinity;
}
tick(elapsed) {
if ('client' === process.env.SIDE) {
this.#nextLetter -= elapsed;
if (this.#nextLetter <= 0) {
this.#thisLetter += 1;
if (this.#thisLetter >= this.#letters.length) {
this.skip();
return;
}
this.showThisLetter();
}
for (let i = 0; i < this.#words.length; ++i) {
this.#words[i].tick(elapsed);
}
}
}
};

View File

@ -0,0 +1,45 @@
const createWord = async (letters, latus) => {
const {Entity} = latus.get('%resources');
const word = await Entity.load({
traits: {
DomNode: {
params: {
style: {
position: 'relative',
},
},
},
},
});
for (let i = 0; i < letters.length; ++i) {
/* eslint-disable no-param-reassign */
letters[i].parentNode = word.node;
letters[i].isBehaving = true;
/* eslint-enable no-param-reassign */
}
const {tick} = word;
word.tick = function T(elapsed) {
tick.call(this, elapsed);
for (let i = 0; i < letters.length; ++i) {
letters[i].tick(elapsed);
}
};
return word;
};
export default async (letters, latus) => {
const words = [];
let word = [];
for (let i = 0; i < letters.length; i++) {
const {text} = letters[i];
word.push(letters[i]);
if (text.match(/[^a-zA-Z0-9_]/)) {
words.push(createWord(word, latus));
word = [];
}
}
if (word.length > 0) {
words.push(createWord(word, latus));
}
return Promise.all(words);
};

View File

@ -0,0 +1,5 @@
import {expect} from 'chai';
it('exists', () => {
expect(true).to.be.true;
});

View File

@ -0,0 +1,3 @@
const neutrino = require('neutrino');
module.exports = neutrino(require(`${__dirname}/.neutrinorc`)).webpack();

8809
packages/dialog/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -31,6 +31,7 @@ export default () => class DomNode extends decorate(Trait) {
static dependencies() { static dependencies() {
return [ return [
'Positioned',
'Visible', 'Visible',
]; ];
} }

View File

@ -6,13 +6,14 @@ export default () => class DomText extends Trait {
constructor() { constructor() {
super(); super();
this.#text = window.document.createElement('span'); if ('client' === process.env.SIDE) {
this.#text = window.document.createElement('span');
}
} }
static defaultParams() { static defaultParams() {
return { return {
centered: true, centered: true,
textStyle: {},
}; };
} }
@ -47,23 +48,28 @@ export default () => class DomText extends Trait {
async load(json) { async load(json) {
await super.load(json); await super.load(json);
if (!this.#text) {
return;
}
const {text} = this.entity; const {text} = this.entity;
this.#text.innerText = text; this.#text.innerText = text;
Object.entries(this.params.textStyle).forEach(([key, value]) => {
this.#text.style[key] = value;
});
this.#text.style.position = 'relative'; this.#text.style.position = 'relative';
if (this.params.centered) { const {fontSize} = this.params;
const {fontSize: fontSizeInPx = '16px'} = this.params.textStyle; if (fontSize) {
const fontSize = parseInt(fontSizeInPx, 10); this.#text.style.fontSize = `${fontSize}px`;
this.#text.style.left = `-${(fontSize / 2) * text.length}px`; if (this.params.centered) {
this.#text.style.top = `-${(fontSize / 2)}px`; this.#text.style.left = `-${(fontSize / 2) * text.length}px`;
this.#text.style.top = `-${(fontSize / 2)}px`;
}
} }
this.entity.node.appendChild(this.#text); this.entity.node.appendChild(this.#text);
} }
onColorChanged() { onColorChanged() {
if (this.entity.is('Colored')) { if (!this.#text) {
return;
}
if (this.entity.is('Colorized')) {
const {red, green, blue} = this.entity; const {red, green, blue} = this.entity;
this.#text.style.color = `rgb(${red}, ${green}, ${blue})`; this.#text.style.color = `rgb(${red}, ${green}, ${blue})`;
} }

View File

@ -0,0 +1,25 @@
import {Trait} from '@avocado/traits';
export default () => class Evolving extends Trait {
#evolving;
static defaultParams() {
return {
lfo: {},
};
}
async load(json) {
await super.load(json);
if (0 === Object.keys(this.params.lfo).length) {
return;
}
this.#evolving = this.entity.lfo(this.params.lfo);
}
tick(elapsed) {
this.#evolving.tick(elapsed);
}
};