├── .babelrc
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── dist
└── vekta.js
├── package-lock.json
├── package.json
├── src
├── _tests
│ └── index.test.js
├── global.js
└── index.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env"]
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/_tests/*.js
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "strict": 0,
4 | "indent": [2, 2],
5 | "quotes": [2, "single"],
6 | "linebreak-style": [2, "unix"],
7 | "semi": [2, "always"],
8 | "no-unused-vars": [2, { "ignoreRestSiblings": true }],
9 | "no-mixed-operators": 0,
10 | "no-plusplus": 0,
11 | "curly": 0,
12 | "no-return-assign": 0
13 | },
14 | "env": {
15 | "es6": true,
16 | "browser": true,
17 | "node": true,
18 | "jest": true
19 | },
20 | "parser": "babel-eslint",
21 | "extends": "eslint:recommended"
22 | }
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lib
8 |
9 | # Runtime data
10 | pids
11 | *.pid
12 | *.seed
13 | *.pid.lock
14 |
15 | # Directory for instrumented libs generated by jscoverage/JSCover
16 | lib-cov
17 |
18 | # Coverage directory used by tools like istanbul
19 | coverage
20 |
21 | # nyc test coverage
22 | .nyc_output
23 |
24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
25 | .grunt
26 |
27 | # Bower dependency directory (https://bower.io/)
28 | bower_components
29 |
30 | # node-waf configuration
31 | .lock-wscript
32 |
33 | # Compiled binary addons (http://nodejs.org/api/addons.html)
34 | build/Release
35 |
36 | # Dependency directories
37 | node_modules/
38 | jspm_packages/
39 |
40 | # Typescript v1 declaration files
41 | typings/
42 |
43 | # Optional npm cache directory
44 | .npm
45 |
46 | # Optional eslint cache
47 | .eslintcache
48 |
49 | # Optional REPL history
50 | .node_repl_history
51 |
52 | # Output of 'npm pack'
53 | *.tgz
54 |
55 | # Yarn Integrity file
56 | .yarn-integrity
57 |
58 | # dotenv environment variables file
59 | .env
60 |
61 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Popmotion
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
2 |
3 | ### A JavaScript vector type with GLSL-inspired swizzling
4 |
5 | ```javascript
6 | const pos = vec2(0); // [0, 0]
7 | pos.y += 1; // [0, 1]
8 | pos.xy = pos.yx; // [1, 0]
9 | const pos3d = vec3(pos, 10); // [1, 0, 10]
10 | ```
11 |
12 | ## Features
13 |
14 | * [Iterable vectors](#iterate)
15 | * [Swizzling](#access)
16 | * [Custom vector types](#create-vector-types)
17 | * Tiny (< 1kb), zero-dependencies
18 |
19 | ## Install
20 |
21 | Via package managers:
22 |
23 | ```bash
24 | npm install vekta --save
25 | ```
26 |
27 | ```bash
28 | yarn add vekta
29 | ```
30 |
31 | Via CDN (Loads Vekta into `window.vekta`):
32 |
33 | ```
34 | https://unpkg.com/vekta/dist/vekta.js
35 | ```
36 |
37 | ## Usage
38 |
39 | ### Import
40 |
41 | All [included types](#included-types) can be imported as named imports, like so:
42 |
43 | ```javascript
44 | import { vec2 } from 'vekta';
45 | ```
46 |
47 | ### Create
48 |
49 | Each vector function returns a proxied array:
50 |
51 | ```javascript
52 | const pos = vec2(0, 10); // [0, 10]
53 | ```
54 |
55 | ### Fill forward
56 |
57 | If the number of provided arguments doesn't match the expected size of the vector, the rest of the array will be filled by cycling through the provided values:
58 |
59 | ```javascript
60 | const pos = vec4(0, 1); // [0, 1, 0, 1]
61 | ```
62 |
63 | ### Access
64 |
65 | These values are accessible both by their index and by their `x`, `y`, `z` and `w` axis labels:
66 |
67 | ```javascript
68 | const pos = vec3(0, 10, 20);
69 | pos[1]; // 10
70 | pos.y; // 10
71 | ```
72 |
73 | This is known as **swizzling** and is inspired by the vector types in [GLSL](https://www.khronos.org/opengl/wiki/Data_Type_%28GLSL%29#Vectors).
74 |
75 | We can return multiple values as a new vector by naming multiple properties:
76 |
77 | ```javascript
78 | pos.xy; // [0, 10]
79 | ```
80 |
81 | These values will be returned in the order defined:
82 |
83 | ```javascript
84 | pos.yx; // [10, 0]
85 | ```
86 |
87 | We can define up to four dimensions to return:
88 |
89 | ```javascript
90 | pos.zzzz; // [20, 20, 20, 20]
91 | ```
92 |
93 | ### Cast into higher dimension
94 |
95 | By passing one vector into another, we can cast existing vectors into higher dimensions.
96 |
97 | ```javascript
98 | const a = vec2(1, 2);
99 | const b = vec3(a, 3); // [1, 2, 3]
100 | const c = vec4(4, b); // [4, 1, 2, 3]
101 | ```
102 |
103 | Combined with swizzling and the `rgba` vector type, we can create a new number by casting only the `rgb` values and providing a new alpha:
104 |
105 | ```javascript
106 | const red = rgba(255, 0, 0, 1);
107 | const semiTransparentRed = rgba(red.rgb, 0.5);
108 | ```
109 |
110 | ### Iterate
111 |
112 | As vectors are just proxied arrays, they offer all the same iterators:
113 |
114 | ```javascript
115 | const pos = vec3(0);
116 | const numAxis = pos.length; // 3
117 |
118 | pos.forEach(/**/);
119 | pos.reduce(/**/);
120 | pos.map(/**/);
121 | ```
122 |
123 | ### Animate
124 |
125 | [Popmotion](https://popmotion.io) can animate arrays, so it can animate vectors:
126 |
127 | ```javascript
128 | tween({ from: pos.xy, to: pos.yx });
129 | ```
130 |
131 | ## Included types
132 |
133 | ### Position: `vec2`, `vec3`, `vec4`
134 |
135 | Property order: `['x', 'y', 'z', 'w']`
136 |
137 | ### Color: `rgb`, `rgba`, `hsl`, `hsla`
138 |
139 | RGBA property order: `['r', 'g', 'b', 'a']`
140 | HSLA property order: `['h', 's', 'l', 'a']`
141 |
142 | **Note:** Currently, the color vectors are essentially syntactic sugar as they don't perform validation to ensure that provided values are valid colors.
143 |
144 | ## Create vector types
145 |
146 | New vector types can be created with the `vectorType` function.
147 |
148 | ```javascript
149 | import { vectorType } from 'vekta';
150 | ```
151 |
152 | `vectorType` accepts an array of unique, alphabetic keys. Each key must be of `length === 1`, and the array itself must be `length >= 2`.
153 |
154 | It returns an array of functions that handle permutations of the vector type of length `2` to `n`, where `n` is the total number of given keys.
155 |
156 | ```javascript
157 | const [foo2, foo3] = vectorType(['a', 'b', 'c']);
158 |
159 | const bar = foo2(0, 10); // [0, 10]
160 | bar.ab = bar.ba; // [10, 0]
161 | bar3 = foo3(20, bar); // [20, 10, 0]
162 | ```
163 |
164 | Currently there isn't a big use case for function, but it's easy to imagine adding a configuration argument that allows us to add a little more intelligence to our vector types like validation.
165 |
166 | ## Browser support
167 |
168 | Vekta requires [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) features that can't be polyfilled so it doesn't offer Internet Explorer support.
169 |
170 | As Googlebot runs Chrome 41, any use of Proxy will prevent client-rendered websites from being crawled correctly.
171 |
172 | ## Roadmap
173 |
174 | Some ideas for future development:
175 |
176 | ### Validated setters
177 |
178 | Add an optional property to `vectorType` that would allow the definition of functions that would transform set properties to ensure only valid props are set:
179 |
180 | ```javascript
181 | const [, rgb, rgba] = vectorType(['r', 'g', 'b', 'a'], {
182 | setters: {
183 | r: pipe(clamp(0, 255), Math.round)
184 | //...the rest
185 | }
186 | });
187 | ```
188 |
189 | ### Interpolation
190 |
191 | Add an optional property to `vectorType` that would allow vector types to interpolate between each other when swizzled.
192 |
193 | ```javascript
194 | const rgb = vectorType(['r', 'g', 'b', 'a'], {
195 | scale: {
196 | r: [0, 255],
197 | g: [0, 255],
198 | b: [0, 255],
199 | a: [0, 1]
200 | }
201 | });
202 |
203 | const color = rgba(255, 255, 255, 0.5);
204 | color.ga = color.ag;
205 | console.log(color); // [255, 127, 255, 1]
206 | ```
207 |
--------------------------------------------------------------------------------
/dist/vekta.js:
--------------------------------------------------------------------------------
1 | !function(r){var e={};function t(n){if(e[n])return e[n].exports;var u=e[n]={i:n,l:!1,exports:{}};return r[n].call(u.exports,u,u.exports,t),u.l=!0,u.exports}t.m=r,t.c=e,t.d=function(r,e,n){t.o(r,e)||Object.defineProperty(r,e,{configurable:!1,enumerable:!0,get:n})},t.r=function(r){Object.defineProperty(r,"__esModule",{value:!0})},t.n=function(r){var e=r&&r.__esModule?function(){return r.default}:function(){return r};return t.d(e,"a",e),e},t.o=function(r,e){return Object.prototype.hasOwnProperty.call(r,e)},t.p="",t(t.s=0)}([function(r,e,t){"use strict";t.r(e);var n={};t.d(n,"vectorType",function(){return f}),t.d(n,"vec2",function(){return i}),t.d(n,"vec3",function(){return l}),t.d(n,"vec4",function(){return a}),t.d(n,"rg",function(){return d}),t.d(n,"rgb",function(){return h}),t.d(n,"rgba",function(){return p}),t.d(n,"hs",function(){return g}),t.d(n,"hsl",function(){return v}),t.d(n,"hsla",function(){return y});const u=(r,e)=>{if(Array.isArray(e)){const t=e.length;for(let n=0;n(e%r+r)%r||0,c=(r,e)=>{const t=(r=>r.reduce(u,[]))(r),n=t.length;if(n===e)return t;t.length=e;for(let r=0;r(r[e]=t,r),f=r=>{const e=r.reduce(s,{}),t=(r=>{const e=r.length,t=e-1,n=(u,o)=>{const c=[u];if(o>=t)return c;for(let t=0;t(r.push(...n(e,0)),r),[]))})(r),n=[],u={get:(r,u,o)=>t.has(u)?((r,e,t,n)=>{const u=t.length;if(1===u)return r[e[t[0]]];{const o=[];for(let n=0;nr[e-2])(n,u)(o)}})(r,e,u,n):Reflect.get(r,u,o),set:(r,n,u,c)=>t.has(n)?((r,e,t,n)=>{const u=t.length,c=Array.isArray(n),s=c?n.length:0;for(let f=0;f{if(t>0){const e=t+1;r.push((...r)=>new Proxy(c(r,e),u))}return r},n)},[i,l,a]=f(["x","y","z","w"]),[d,h,p]=f(["r","g","b","a"]),[g,v,y]=f(["h","s","l","a"]);window.vekta=n}]);
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vekta",
3 | "version": "0.2.0",
4 | "description": "A JavaScript vector library with GLSL-inspired swizzling",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "test": "jest",
8 | "test-watch": "jest --watch",
9 | "build": "babel src -d lib && npx webpack --config webpack.config.js",
10 | "lint": "eslint src",
11 | "measure": "gzip -c dist/vekta.js | wc -c",
12 | "prepublish": "npm run lint && npm run test && npm run build"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/Popmotion/vekta.git"
17 | },
18 | "keywords": [
19 | "vector"
20 | ],
21 | "author": "Matt Perry",
22 | "license": "MIT",
23 | "bugs": {
24 | "url": "https://github.com/Popmotion/vekta/issues"
25 | },
26 | "homepage": "https://github.com/Popmotion/vekta#readme",
27 | "jest": {
28 | "testRegex": "(/_tests/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
29 | "moduleFileExtensions": [
30 | "js",
31 | "jsx",
32 | "json",
33 | "node"
34 | ],
35 | "rootDir": "src"
36 | },
37 | "devDependencies": {
38 | "babel-cli": "^6.26.0",
39 | "babel-core": "^6.26.0",
40 | "babel-eslint": "^8.2.3",
41 | "babel-jest": "^22.4.3",
42 | "babel-preset-env": "^1.6.1",
43 | "eslint": "^4.19.1",
44 | "eslint-config-airbnb-base": "^12.1.0",
45 | "eslint-plugin-import": "^2.11.0",
46 | "jest": "^22.4.3",
47 | "regenerator-runtime": "^0.11.1",
48 | "webpack": "^4.5.0",
49 | "webpack-cli": "^2.0.14"
50 | },
51 | "unpkg": "./dist/vekta.js",
52 | "prettier": {
53 | "singleQuote": true
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/_tests/index.test.js:
--------------------------------------------------------------------------------
1 | import { vec2, vec3, vec4 } from '../';
2 |
3 | test('vec()', () => {
4 | console.log();
5 | const v = vec2(10, 20);
6 |
7 | // Creation
8 | expect(vec2(10)).toEqual([10, 10]);
9 | expect(v).toEqual([10, 20]);
10 | expect(vec4(1, 2, 3, 4)).toEqual([1, 2, 3, 4]);
11 | expect(vec4([1, 2, 3, 4])).toEqual([1, 2, 3, 4]);
12 | expect(vec4(vec2([1, 2]), vec2([3, 4]))).toEqual([1, 2, 3, 4]);
13 | expect(vec4(1, 2)).toEqual([1, 2, 1, 2]);
14 | expect(vec2([1, 2])).toEqual([1, 2]);
15 | expect(vec4([1, 2])).toEqual([1, 2, 1, 2]);
16 |
17 | // Get
18 | expect(v.length).toEqual(2);
19 | expect(v.x).toEqual(10);
20 | expect(v.yx).toEqual([20, 10]);
21 | expect(v.xxxx).toEqual([10, 10, 10, 10]);
22 |
23 | // Set
24 | v.y = 30;
25 | expect(v.y).toEqual(30);
26 | const v3 = vec3(10);
27 | expect(v3.y).toEqual(10);
28 | v3.xyz = 20;
29 | expect(v3).toEqual([20, 20, 20]);
30 | v3.xyz = [1, 2];
31 | expect(v3.x).toEqual(1);
32 | expect(v3.y).toEqual(2);
33 | expect(v3.z).toEqual(1);
34 |
35 | const a = vec2(1, 2);
36 | const b = vec4(5, 5, 5, 5);
37 | b.yz = a;
38 | expect(b).toEqual([5, 1, 2, 5]);
39 | const { x, y } = a;
40 | expect(x).toEqual(1);
41 |
42 | // Enumerate
43 | expect(a.map(v => v)).toEqual([1, 2]);
44 | });
45 |
--------------------------------------------------------------------------------
/src/global.js:
--------------------------------------------------------------------------------
1 | import * as vekta from './';
2 |
3 | window.vekta = vekta;
4 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // Flatten an array of values
2 | const flattenItem = (acc, val) => {
3 | if (Array.isArray(val)) {
4 | const numItems = val.length;
5 | for (let i = 0; i < numItems; i++) acc.push(val[i]);
6 | } else {
7 | acc.push(val);
8 | }
9 |
10 | return acc;
11 | };
12 |
13 | const flatten = arr => arr.reduce(flattenItem, []);
14 |
15 | /**
16 | * Create a valid set of swizzles based on a list of single-character keys.
17 | * ie ['x', 'y'] -> ['x', 'y', 'xx', 'xy' ...]
18 | */
19 | const createValidSwizzleSet = (keys) => {
20 | const numKeys = keys.length;
21 | const maxDepth = numKeys - 1;
22 |
23 | const addKeys = (key, depth) => {
24 | const validKeys = [key];
25 |
26 | if (depth >= maxDepth) return validKeys;
27 |
28 | for (let i = 0; i < numKeys; i++) {
29 | validKeys.push(...addKeys(key + keys[i], depth + 1));
30 | }
31 |
32 | return validKeys;
33 | };
34 |
35 | return new Set(keys.reduce((acc, key) => {
36 | acc.push(...addKeys(key, 0));
37 | return acc;
38 | }, []));
39 | };
40 |
41 | const getWrappedIndex = (total, i) => (i % total + total) % total || 0;
42 |
43 | // Fill a given array to the size provided with
44 | // a repeating pattern of its existing values. ie ([10, 20], 3) -> [10, 20, 10]
45 | const fillVectorArray = (values, size) => {
46 | const newValues = flatten(values);
47 | const numValues = newValues.length;
48 |
49 | // If we already have the correct number of values, return early
50 | if (numValues === size) return newValues;
51 |
52 | // Set array to correct size
53 | newValues.length = size;
54 |
55 | // Fill in the blanks
56 | for (let i = 0; i < size; i++) {
57 | if (newValues[i] === undefined)
58 | newValues[i] = newValues[getWrappedIndex(numValues, i)];
59 | }
60 |
61 | return newValues;
62 | };
63 |
64 | const makeIndicesMap = (map, key, i) => ((map[key] = i), map);
65 |
66 | const getVectorFactory = (factories, size) => factories[size - 2];
67 |
68 | // TODO: Replace .split('') with a for loop
69 | const getSwizzled = (target, indices, key, vec) => {
70 | const numKeys = key.length;
71 |
72 | if (numKeys === 1) {
73 | return target[indices[key[0]]];
74 | } else {
75 | const values = [];
76 | for (let i = 0; i < numKeys; i++) values.push(target[indices[key[i]]]);
77 | return getVectorFactory(vec, numKeys)(values);
78 | }
79 | };
80 |
81 | const setSwizzled = (target, indices, key, value) => {
82 | const numKeys = key.length;
83 | const valueIsArray = Array.isArray(value);
84 | const numValues = valueIsArray ? value.length : 0;
85 |
86 | for (let i = 0; i < numKeys; i++) {
87 | target[indices[key[i]]] = valueIsArray
88 | ? value[getWrappedIndex(numValues, i)]
89 | : value;
90 | }
91 | return value;
92 | };
93 |
94 | const vectorType = axisOrder => {
95 | const indices = axisOrder.reduce(makeIndicesMap, {});
96 | const validSwizzleKeys = createValidSwizzleSet(axisOrder);
97 | const vecFactories = [];
98 |
99 | const vectorProxy = {
100 | get(target, key, receiver) {
101 | return validSwizzleKeys.has(key)
102 | ? getSwizzled(target, indices, key, vecFactories)
103 | : Reflect.get(target, key, receiver);
104 | },
105 | set(target, key, value, receiver) {
106 | return validSwizzleKeys.has(key)
107 | ? setSwizzled(target, indices, key, value)
108 | : Reflect.set(target, key, value, receiver);
109 | }
110 | };
111 |
112 | return axisOrder.reduce((acc, _, i) => {
113 | // No vector factories for 1-dimensions
114 | if (i > 0) {
115 | const size = i + 1;
116 | acc.push(
117 | (...values) => new Proxy(fillVectorArray(values, size), vectorProxy)
118 | );
119 | }
120 | return acc;
121 | }, vecFactories);
122 | };
123 |
124 | export { vectorType };
125 | export const [vec2, vec3, vec4] = vectorType(['x', 'y', 'z', 'w']);
126 | export const [rg, rgb, rgba] = vectorType(['r', 'g', 'b', 'a']);
127 | export const [hs, hsl, hsla] = vectorType(['h', 's', 'l', 'a']);
128 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: './src/global.js',
5 | output: {
6 | filename: 'vekta.js',
7 | path: path.resolve(__dirname, 'dist')
8 | }
9 | };
10 |
--------------------------------------------------------------------------------