Comparing version 0.3.0 to 0.4.0
{ | ||
"name": "fauna", | ||
"version": "0.3.0", | ||
"version": "0.4.0", | ||
"description": "Generate and render animated L-Systems", | ||
@@ -10,2 +10,3 @@ "main": "src/index.js", | ||
"test:single": "mocha src/*.spec.js", | ||
"generate-testdata": "node src/util.js", | ||
"semantic-release": "semantic-release pre && npm publish && semantic-release post" | ||
@@ -12,0 +13,0 @@ }, |
@@ -24,1 +24,5 @@ # fauna | ||
TODO: Test `git tag -a <semver> -m "my version 1.4"` to test if it creates the release message in Github | ||
- [ ] Add function to ingest and run config | ||
- [ ] Documentation | ||
- [ ] Landing page | ||
- [ ] Finished egghead.io publishing guide |
176
src/index.js
@@ -0,1 +1,5 @@ | ||
const xml = require('xml'); | ||
// Takes an array of commands (stack) and returns a corresponding | ||
// svg path string. | ||
function pathString(stack) { | ||
@@ -9,13 +13,16 @@ let path = []; | ||
function boundingBox(stack) { | ||
// Calculates the bounding box of an array of commands. | ||
function boundingBox(stacks) { | ||
let x = y = 0; | ||
let minX = minY = Infinity; | ||
let maxX = maxY = -Infinity; | ||
stack.forEach(function(p) { | ||
x = (p.c === 'M') ? p.x : x + p.x; | ||
y = (p.c === 'M') ? p.y : x + p.y; | ||
minX = Math.min(minX, x); | ||
minY = Math.min(minY, y); | ||
maxX = Math.max(maxX, x); | ||
maxY = Math.max(maxY, y); | ||
stacks.forEach(function(stack) { | ||
stack.forEach(function(p) { | ||
x = (p.c === 'M') ? p.x : x + p.x; | ||
y = (p.c === 'M') ? p.y : y + p.y; | ||
minX = Math.min(minX, x); | ||
minY = Math.min(minY, y); | ||
maxX = Math.max(maxX, x); | ||
maxY = Math.max(maxY, y); | ||
}); | ||
}); | ||
@@ -25,4 +32,87 @@ return {'minX': minX, 'minY': minY, 'maxX': maxX, 'maxY': maxY}; | ||
// Calculates the overall length of array of commands. | ||
// TODO(bradleybossard) : Handle M's. | ||
function pathLength(stack) { | ||
let length = 0.0; | ||
let x = y = 0; | ||
stack.forEach(function(p) { | ||
if (p.c == 'l') { | ||
let xDiff = p.x - x; | ||
let yDiff = p.y - y; | ||
length += Math.sqrt(Math.pow(xDiff, 2) + Math.pow(yDiff, 2)); | ||
} | ||
({x, y} = p); | ||
}); | ||
return length; | ||
} | ||
// Returns an object respresnting an svg <animate> element. | ||
function animateElement(fromPath, toPath, duration) { | ||
const valuesPath = `${fromPath};${toPath};${fromPath};`; | ||
return [{animate: {_attr:{ | ||
attributeName: 'd', | ||
begin: '0s', | ||
dur: duration, | ||
values: valuesPath, | ||
repeatCount: 'indefinite' | ||
}}}]; | ||
} | ||
// Returns an object respresenting an svg <path> element. | ||
function pathElement(path, name, minX, minY, animateEls) { | ||
const attrs = { | ||
d : path, | ||
id: name, | ||
transform: `translate(${-minX},${-minY})`, | ||
class: name | ||
}; | ||
let root = [ | ||
{_attr: attrs}, | ||
]; | ||
animateEls.forEach(function(el) { | ||
root.push({'animate': el}); | ||
}); | ||
return root; | ||
} | ||
// Shuffles a stack of commands. | ||
function shufflePath(stack) { | ||
// TODO(bradleybossard) : Implment this function | ||
} | ||
// Returns an object respresenting an svg <path> element. | ||
function renderPath(stacks, pathName) { | ||
let animateEls = []; | ||
const fromStack = stacks[0]; | ||
const box = boundingBox(stacks); | ||
const fromPath = pathString(fromStack); | ||
const fromLength = pathLength(fromStack); | ||
if (stacks.length > 1) { | ||
const toStack = stacks[1]; | ||
const animateEl = animateElement(fromPath, toPath, 10); | ||
animateEls.push(animateEl); | ||
} | ||
const pathSvg = pathElement(fromPath, pathName, box.minX, box.minY, animateEls); | ||
return {path: pathSvg, box: box, length: fromLength}; | ||
} | ||
// Returns an object respresenting an svg <style> element. | ||
function styleElement(props, pathName) { | ||
return `.${pathName} { | ||
stroke: ${props.stroke}; | ||
stroke-linecap: ${props['stroke-linecap']}; | ||
stroke-linejoin: ${props['stroke-linejoin']}; | ||
stroke-width: ${props['stroke-width']}; | ||
stroke-opacity: ${props['stroke-opacity']}; | ||
stroke-dasharray: ${props['stroke-dasharray']}; | ||
stroke-dashoffset: ${props['stroke-dashoffset']}; | ||
}`; | ||
} | ||
// Generates the iterated rules string. | ||
exports.iterate = function(axiom, rules, iterations) { | ||
for (var i = 0; i < iterations; i++) { | ||
for (let i = 0; i < iterations; i++) { | ||
axiom = axiom.replace(/\w/g, function(c) { | ||
@@ -35,2 +125,3 @@ return rules[c] || c; | ||
// Loops through iterated rules string to produce svg-like path commands. | ||
exports.toCommands = function(length, alpha, lengthGrowth, alphaGrowth, stream) { | ||
@@ -44,3 +135,3 @@ let point = {'x': 0, 'y': 0}; | ||
for(var i = 0, c =''; c = stream.charAt(i); i++){ | ||
for(let i = 0, c =''; c = stream.charAt(i); i++) { | ||
switch(c) { | ||
@@ -60,5 +151,7 @@ case '(': | ||
case 'F': | ||
const deltaX = lineLength * Math.cos(Math.PI / 180 * angle) | ||
const deltaY = lineLength * Math.sin(Math.PI / 180 * angle) | ||
stack.push({'c': 'l', 'x': 0, 'y': 0}); | ||
let deltaX = lineLength * Math.cos(Math.PI / 180 * angle) | ||
let deltaY = lineLength * Math.sin(Math.PI / 180 * angle) | ||
deltaX = Math.abs(deltaX) < 0.000001 ? 0 : deltaX; | ||
deltaY = Math.abs(deltaY) < 0.000001 ? 0 : deltaY; | ||
stack.push({'c': 'l', 'x': deltaX, 'y': deltaY}); | ||
point.x += deltaX; | ||
@@ -77,3 +170,4 @@ point.y += deltaY; | ||
case ']': | ||
angle, point, alpha = tempStack.pop() | ||
({angle, point, alpha} = tempStack.pop()); | ||
stack.push({'c': 'M', 'x': point.x, 'y': point.y}); | ||
break; | ||
@@ -91,7 +185,53 @@ case '!': | ||
exports.toPaths = function(stack) { | ||
const xml = require('xml'); | ||
return pathString(stack); | ||
// Takes an array of command stacks, a name and style to create | ||
// a finished svg string. | ||
exports.toSvg = function(stacks, pathName, props) { | ||
const path = renderPath(stacks, pathName); | ||
const style = styleElement(props, pathName); | ||
const svgWidth = path.box.maxX - path.box.minX; | ||
const svgHeight = path.box.maxY - path.box.minY; | ||
const attrs = { | ||
'viewBox': `0 0 ${svgWidth} ${svgHeight}`, | ||
'width': svgWidth, | ||
'height': svgHeight, | ||
'xmlns:cc': 'http://creativecommons.org/ns#', | ||
'xmlns:dc': 'http://purl.org/dc/elements/1.1', | ||
'xmlns:rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', | ||
'xmlns:svg': 'http://www.w3.org/2000/svg', | ||
'xmlns:xlink' : 'http://www.w3.org/1999/xlink', | ||
'xmlns': 'http://www.w3.org/2000/svg', | ||
}; | ||
const use = {'_attr': { | ||
'x': 0, | ||
'y': 0, | ||
'class': pathName, | ||
'xlink:href': '#' + pathName | ||
}}; | ||
const defs = [{path: path.path}]; | ||
let root = [{svg: [ | ||
{_attr: attrs}, | ||
{style: style}, | ||
{use: use}, | ||
{defs: defs} | ||
]}]; | ||
return xml(root); | ||
} | ||
// Produces an svg based on a config file. | ||
exports.runConfig = function(config) { | ||
// TODO(bradleybossard): Validate fields of config | ||
let stacks = []; | ||
config.patterns.forEach(function(pattern) { | ||
const stream = exports.iterate(pattern.axiom, pattern.rules, pattern.iterations); | ||
const stack = exports.toCommands(pattern.length, pattern.alpha, pattern.lengthGrowth, pattern.alphaGrowth, stream); | ||
stacks.push(stack); | ||
}); | ||
return exports.toSvg(stacks, config.meta.name, config.style); | ||
} |
const expect = require('chai').expect; | ||
const rewire = require('rewire'); | ||
const fauna = rewire('./index.js'); | ||
const util = require('util'); | ||
const fs = require('fs'); | ||
const pathString = fauna.__get__('pathString'); | ||
const boundingBox = fauna.__get__('boundingBox'); | ||
const animateElement = fauna.__get__('animateElement'); | ||
const pathElement = fauna.__get__('pathElement'); | ||
const pathLength = fauna.__get__('pathLength'); | ||
const renderPath = fauna.__get__('renderPath'); | ||
const styleElement = fauna.__get__('styleElement'); | ||
describe('iterate test', function() { | ||
it('simple iteration', function(done) { | ||
const expandedString = fauna.iterate('L', {'L': 'LFLR+'}, 2); | ||
expect(expandedString).to.be.equal('LFLR+FLFLR+R+'); | ||
it('should iterate properly 1', function(done) { | ||
const actual = fauna.iterate('L', {'L': 'LFLR+'}, 2); | ||
const expected = 'LFLR+FLFLR+R+'; | ||
expect(actual).to.be.equal(expected); | ||
done(); | ||
}); | ||
it('simple iteration', function(done) { | ||
const expandedString = fauna.iterate('a', {'a': 'bac', 'c': 'ddd'}, 4); | ||
expect(expandedString).to.be.equal('bbbbacddddddddd'); | ||
it('should iterate properly 2', function(done) { | ||
const actual = fauna.iterate('a', {'a': 'bac', 'c': 'ddd'}, 4); | ||
const expected = 'bbbbacddddddddd'; | ||
expect(actual).to.be.equal(expected); | ||
done(); | ||
@@ -24,8 +33,8 @@ }); | ||
it('covert stream', function(done) { | ||
const expected = [{ c: 'M', x: 0, y: 0 }, | ||
{ c: 'l', x: 0, y: 0 }, | ||
{ c: 'l', x: 0, y: 0 }, | ||
{ c: 'l', x: 0, y: 0 }]; | ||
const commands = fauna.toCommands(1, 30, 0.1, 0.1, 'LFLR+FLFLR+R+'); | ||
expect(commands).to.be.deep.equal(expected); | ||
const expected = [ { c: 'M', x: 0, y: 0 }, | ||
{ c: 'l', x: 0, y: -1 }, | ||
{ c: 'l', x: 1, y: 0 }, | ||
{ c: 'l', x: 1, y: 0 } ]; | ||
const actual = fauna.toCommands(1, 90, 0.1, 0.1, 'LFLR+FLFLR+R+'); | ||
expect(actual).to.be.deep.equal(expected); | ||
done(); | ||
@@ -42,4 +51,4 @@ }); | ||
const expected = 'M 0 0 l 0 0 l 0 0 l 0 0'; | ||
const path = pathString(stack); | ||
expect(path).to.be.equal(expected); | ||
const actual = pathString(stack); | ||
expect(actual).to.be.equal(expected); | ||
done(); | ||
@@ -51,9 +60,14 @@ }); | ||
it('should calculate the bounding box properly', function(done) { | ||
const stack = [{ c: 'M', x: -4, y: 0 }, | ||
{ c: 'M', x: 0, y: -4 }, | ||
{ c: 'M', x: 4, y: 0 }, | ||
{ c: 'M', x: 0, y: 4 }]; | ||
const expected = {'minX': -4, 'minY': -4, 'maxX': 4, 'maxY': 4}; | ||
const box = boundingBox(stack); | ||
expect(box).to.be.deep.equal(expected); | ||
const stack1 = [{ c: 'M', x: -4, y: 0 }, | ||
{ c: 'M', x: 0, y: -4 }, | ||
{ c: 'M', x: 4, y: 0 }, | ||
{ c: 'M', x: 0, y: 4 }]; | ||
const stack2 = [{ c: 'M', x: -8, y: 0 }, | ||
{ c: 'M', x: 0, y: -8 }, | ||
{ c: 'M', x: 0, y: 0 }, | ||
{ c: 'M', x: 0, y: 0 }]; | ||
const stacks = [stack1, stack2]; | ||
const expected = {'minX': -8, 'minY': -8, 'maxX': 4, 'maxY': 4}; | ||
const actual = boundingBox(stacks); | ||
expect(actual).to.be.deep.equal(expected); | ||
done(); | ||
@@ -63,4 +77,102 @@ }); | ||
describe('animationElement test', function() { | ||
it('should produce an animate xml element properly', function(done) { | ||
const fromPath = '1 2 1'; | ||
const toPath = '3 4 3'; | ||
const duration = 20; | ||
const expected = [{animate: {_attr:{ | ||
attributeName: 'd', | ||
begin: '0s', | ||
dur: 20, | ||
values: '1 2 1;3 4 3;1 2 1;', | ||
repeatCount: 'indefinite' | ||
}}}]; | ||
const actual = animateElement(fromPath, toPath, duration); | ||
expect(actual).to.be.deep.equal(expected); | ||
done(); | ||
}); | ||
}); | ||
describe('pathElement test', function() { | ||
it('should produce an path xml element properly', function(done) { | ||
const path = '1 2 1'; | ||
const name = 'pathname'; | ||
const minX = minY = -10; | ||
const animateEls = [{animate: {_attr:{ | ||
attributeName: 'd', | ||
begin: '0s', | ||
dur: 20, | ||
values: '1 2 1;3 4 3;1 2 1;', | ||
repeatCount: 'indefinite' | ||
}}}]; | ||
const expected = [ { _attr: { d: '1 2 1', id: 'pathname', transform: 'translate(10,10)', class: 'pathname' } }, { animate: { animate: { _attr: { attributeName: 'd', begin: '0s', dur: 20, values: '1 2 1;3 4 3;1 2 1;', repeatCount: 'indefinite' } } } } ]; | ||
const actual = pathElement(path, name, minX, minY, animateEls); | ||
//console.log(util.inspect(actual, false, null)); | ||
expect(actual).to.be.deep.equal(expected); | ||
done(); | ||
}); | ||
}); | ||
describe('pathLength test', function() { | ||
it('should calculate the length of a path', function(done) { | ||
const stack = [{c: 'M', x:0, y:0}, {c: 'l', x:3, y:3}]; | ||
const expected = Math.sqrt(18); | ||
const actual = pathLength(stack); | ||
expect(actual).to.be.equal(expected); | ||
done(); | ||
}); | ||
}); | ||
describe('renderPath test', function() { | ||
it('should render path correctly', function(done) { | ||
const pathName = 'test1'; | ||
const stack1 = [{c: 'M', x:0, y:0}, {c: 'l', x:3, y:3}]; | ||
const stacks = [stack1]; | ||
const expected = { path: [ { _attr: { d: 'M 0 0 l 3 3', id: 'test1',transform: 'translate(0,0)',class: 'test1' } } ],box: { minX: 0, minY: 0, maxX: 3, maxY: 3 },length: 4.242640687119285 }; | ||
const actual = renderPath(stacks, pathName); | ||
//console.log(util.inspect(actual, false, null)); | ||
expect(actual).to.be.deep.equal(expected); | ||
done(); | ||
}); | ||
}); | ||
describe('styleElement test', function() { | ||
it('should produce style element correctly', function(done) { | ||
const props = { | ||
'stroke': '#FFF', | ||
'stroke-linecap': 'butt', | ||
'stroke-linejoin': 'miter', | ||
'stroke-width': '1px', | ||
'stroke-opacity': '1.0', | ||
'stroke-dasharray': '20 20', | ||
'stroke-dashoffset': '10.0', | ||
}; | ||
const pathName = 'test1'; | ||
const expected = '.test1 {\n stroke: #FFF;\n stroke-linecap: butt;\n stroke-linejoin: miter;\n stroke-width: 1px;\n stroke-opacity: 1.0;\n stroke-dasharray: 20 20;\n stroke-dashoffset: 10.0;\n }'; | ||
const actual = styleElement(props, pathName); | ||
expect(actual).to.be.deep.equal(expected); | ||
done(); | ||
}); | ||
}); | ||
describe('toSvg test', function() { | ||
it('should produce an SVG', function(done) { | ||
const pathName = 'path1'; | ||
const props = JSON.parse(fs.readFileSync('./src/testdata/props.json', 'utf8')); | ||
const stacks = JSON.parse(fs.readFileSync('./src/testdata/stacks.json', 'utf8')); | ||
const expected = fs.readFileSync('./src/testdata/expected.svg', 'utf8'); | ||
const actual = fauna.toSvg(stacks, pathName, props); | ||
expect(actual).to.be.equal(expected); | ||
done(); | ||
}); | ||
}); | ||
describe('runConfig test', function() { | ||
it('should produce an SVG', function(done) { | ||
const config = JSON.parse(fs.readFileSync('./configs/hilbert.json', 'utf8')); | ||
const expected = fs.readFileSync('./src/testdata/hilbert-expected.svg', 'utf8'); | ||
const actual = fauna.runConfig(config); | ||
expect(actual).to.be.equal(expected); | ||
done(); | ||
}); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
28624
12
446
28
2
1