├── .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 | # Vekta 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 | --------------------------------------------------------------------------------