├── rainbow-wave.gif ├── .gitignore ├── src ├── main.js ├── base-animation.js ├── __tests__ │ ├── extra.test.js │ ├── color.test.js │ ├── main.test.js │ └── __snapshots__ │ │ └── main.test.js.snap ├── extra.js ├── create-frame-iterator.js ├── color.js └── render-dom.js ├── rollup.config.js ├── README.md └── package.json /rainbow-wave.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grough/pixel-animator/HEAD/rainbow-wave.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | main.js 4 | extra.js 5 | umd 6 | .cache 7 | .DS_Store 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createFrameIterator } from './create-frame-iterator'; 2 | import { renderAnimatedDom } from './render-dom'; 3 | import { baseAnimation } from './base-animation'; 4 | 5 | export default function PixelAnimator(userAnimation, domNode) { 6 | const animation = { ...baseAnimation, ...userAnimation }; 7 | if (domNode) return renderAnimatedDom(animation, domNode); 8 | return createFrameIterator(animation); 9 | } 10 | -------------------------------------------------------------------------------- /src/base-animation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a default animation that will render if no user animation is 3 | * given. It looks like an alternating checkerboard. 4 | */ 5 | export const baseAnimation = { 6 | columns: 16, 7 | rows: 16, 8 | frames: Infinity, 9 | frameRate: 8, 10 | colorize: ({ column, row, frame }) => 11 | Math.floor(column + row + frame / 8) % 2 === 0 12 | ? frame % 2 === 0 13 | ? 0.92 14 | : 0.94 15 | : 0.975, 16 | }; 17 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | 3 | export default [ 4 | { 5 | input: 'src/main.js', 6 | output: { 7 | file: __dirname + '/main.js', 8 | format: 'umd', 9 | name: 'PixelAnimator', 10 | }, 11 | plugins: [babel()], 12 | }, 13 | { 14 | input: 'src/extra.js', 15 | output: { 16 | file: __dirname + '/extra.js', 17 | format: 'umd', 18 | name: 'PixelAnimatorExtra', 19 | }, 20 | plugins: [babel()], 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /src/__tests__/extra.test.js: -------------------------------------------------------------------------------- 1 | import { scale, intersect } from "../extra"; 2 | 3 | it("should map a value from one range to another", () => { 4 | expect(scale(0.5, 0, 1, 10, 20)).toBe(15); 5 | expect(scale(1.5, 1, 2, 0, 1)).toBe(0.5); 6 | }); 7 | 8 | it("should determine whether a point intersects a space", () => { 9 | const square = intersect({ left: -1, right: 1, top: -1, bottom: 1 }); 10 | expect(square(0, 0)).toBe(true); 11 | expect(square(-0.999, 0.999)).toBe(true); 12 | expect(square(-1, 1)).toBe(true); 13 | expect(square(-1.001, 1.001)).toBe(false); 14 | expect(square(1, 1)).toBe(true); 15 | 16 | const right = intersect({ left: 0 }); 17 | expect(right(0, 0)).toBe(true); 18 | expect(right(1, 0)).toBe(true); 19 | expect(right(-1, 0)).toBe(false); 20 | expect(right(-1, 1)).toBe(false); 21 | expect(right(-1, -1)).toBe(false); 22 | }); 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Pixel Animator** is a toy for making colorful, animated pixel art with JavaScript. Kind of like graphics programming but less powerful and more fun. 2 | 3 | [![Rainbow sine wave 256×5×16](rainbow-wave.gif)](https://glitch.com/~wavey-spectrum) 4 | 5 | ## Getting Started 6 | 7 | ### The easy way 8 | 9 | 1. Start with a template on [Glitch](https://glitch.com/edit/#!/remix/pixel-animator-template) or [CodePen](https://codepen.io/pen?template=JjdoYoa&editors=1000) 10 | 2. Read the [tutorial](https://github.com/grough/pixel-animator/wiki/Pixel-Animator-Tutorial) 11 | 3. Enjoy? 12 | 13 | If you want to make your own web page or install the Node.js module, take a look at the [installation notes](https://github.com/grough/pixel-animator/wiki/Installation). 14 | 15 | ## There's more… 16 | 17 | Some features aren't documented yet. The [wiki](https://github.com/grough/pixel-animator/wiki) is under development. 18 | -------------------------------------------------------------------------------- /src/__tests__/color.test.js: -------------------------------------------------------------------------------- 1 | import { normalizeColor } from "../color"; 2 | 3 | it("should normalize a color name", () => { 4 | expect(normalizeColor("red")).toEqual({ 5 | alpha: 1, 6 | blue: 54, 7 | green: 65, 8 | red: 255, 9 | }); 10 | }); 11 | 12 | it("should normalize a hex code", () => { 13 | expect(normalizeColor("#FF0000")).toEqual({ 14 | alpha: 1, 15 | blue: 0, 16 | green: 0, 17 | red: 255, 18 | }); 19 | }); 20 | 21 | it("should normalize a partial RGBA object", () => { 22 | expect(normalizeColor({ red: 1 })).toEqual({ 23 | alpha: 1, 24 | blue: 0, 25 | green: 0, 26 | red: 255, 27 | }); 28 | }); 29 | 30 | it("should normalize a complete RGBA object", () => { 31 | expect( 32 | normalizeColor({ red: 0.1, green: 0.2, blue: 0.3, alpha: 0.4 }), 33 | ).toEqual({ 34 | alpha: 0, 35 | blue: 76, 36 | green: 51, 37 | red: 25, 38 | }); 39 | }); 40 | 41 | it("should normalize a number", () => { 42 | expect(normalizeColor(0.5)).toEqual({ 43 | alpha: 1, 44 | blue: 127, 45 | green: 127, 46 | red: 127, 47 | }); 48 | }); 49 | 50 | it("should normalize a null value", () => { 51 | expect(normalizeColor(null)).toEqual({ 52 | alpha: 0, 53 | blue: 0, 54 | green: 0, 55 | red: 0, 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/extra.js: -------------------------------------------------------------------------------- 1 | export function scale(v, a, b, c, d) { 2 | return (-v * c + c * b + v * d - a * d) / (b - a); 3 | } 4 | 5 | /* 6 | * Add polar coordinates `angle` and `distance` with respect to an origin 7 | * at the center of the frame. 8 | */ 9 | export function polar(colorize) { 10 | return ({ column, row, columns, rows, ...rest }) => { 11 | const x = scale(column, 0, columns - 1, -1, 1); 12 | const y = scale(row, 0, rows - 1, -1, 1); 13 | const theta = Math.atan2(y, x); 14 | const angle = theta > 0 ? theta : theta + 2 * Math.PI; 15 | const distance = Math.sqrt(x * x + y * y); 16 | return colorize({ 17 | ...rest, 18 | column, 19 | row, 20 | columns, 21 | rows, 22 | angle, 23 | distance, 24 | x, 25 | y, 26 | }); 27 | }; 28 | } 29 | 30 | /* 31 | * Given a bounding box, return a function that tells you whether a given 32 | * point intersects with that box. 33 | */ 34 | export function intersect(boundary) { 35 | const { left, right, top, bottom } = { 36 | left: -Infinity, 37 | right: Infinity, 38 | top: -Infinity, 39 | bottom: Infinity, 40 | ...boundary, 41 | }; 42 | return function(x, y) { 43 | return x >= left && x <= right && y >= top && y <= bottom; 44 | }; 45 | } 46 | 47 | /* 48 | * Add `phase` property to express time from the beginning to the end of an 49 | * an animation as a number between 0 and 1. 50 | */ 51 | export function phase(colorize) { 52 | return ({ frame, frames, ...rest }) => { 53 | return colorize({ 54 | ...rest, 55 | frame, 56 | frames, 57 | phase: frames === Infinity ? 0 : scale(frame, 0, frames - 1, 0, 1), 58 | }); 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/create-frame-iterator.js: -------------------------------------------------------------------------------- 1 | import { normalizeColor } from './color'; 2 | 3 | function mod(x, n) { 4 | return ((x % n) + n) % n; 5 | } 6 | 7 | function index(column, row, columns, rows) { 8 | return mod(row, rows) * columns + mod(column, columns); 9 | } 10 | 11 | function createCellReader(columns, rows, cellData) { 12 | return (column, row) => cellData[index(column, row, columns, rows)]; 13 | } 14 | 15 | /* 16 | * Return a function that, when called, will advance to the next animation 17 | * frame, calculate all the cell colours for that frame and return them. 18 | * Calling the function again will return the subsequent frame, and so on. 19 | */ 20 | export function createFrameIterator(animation) { 21 | const { columns, rows, frames, evolve, colorize } = animation; 22 | let cellData = []; 23 | let cellDataPrevious; 24 | let cellColors; 25 | let cellReader; 26 | let frame = 0; 27 | return function generateNextFrame() { 28 | cellDataPrevious = cellData; 29 | cellReader = createCellReader(columns, rows, cellDataPrevious); 30 | cellData = []; 31 | cellColors = []; 32 | for (let row = 0; row < rows; row++) { 33 | for (let column = 0; column < columns; column++) { 34 | const context = { 35 | columns, 36 | rows, 37 | frames, 38 | column, 39 | row, 40 | frame: frame % frames, 41 | cells: cellReader, 42 | }; 43 | const cell = evolve ? evolve(context) : undefined; 44 | cellData.push(cell); 45 | const color = colorize({ ...context, cell }); 46 | cellColors = cellColors.concat(normalizeColor(color)); 47 | } 48 | } 49 | frame++; 50 | return cellColors; 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixel-animator", 3 | "version": "1.1.14", 4 | "description": "Make generative pixel art in your browser", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/grough/pixel-animator" 8 | }, 9 | "author": "Gavin Rough ", 10 | "main": "main.js", 11 | "scripts": { 12 | "test": "jest src/**/*.js --runInBand --watchAll", 13 | "build": "rollup --config rollup.config.js", 14 | "lint": "eslint --fix src/**/*.js", 15 | "format": "prettier --write src/**/*.js" 16 | }, 17 | "files": [ 18 | "main.js", 19 | "extra.js" 20 | ], 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@babel/plugin-proposal-object-rest-spread": "^7.10.3", 24 | "@babel/preset-env": "^7.10.3", 25 | "babel-jest": "^26.1.0", 26 | "eslint": "^7.3.1", 27 | "husky": "^4.2.5", 28 | "jest": "^26.1.0", 29 | "prettier": "^2.0.5", 30 | "rollup": "^2.18.0", 31 | "rollup-plugin-babel": "^4.4.0" 32 | }, 33 | "babel": { 34 | "presets": [ 35 | "@babel/preset-env" 36 | ], 37 | "plugins": [ 38 | "@babel/plugin-proposal-object-rest-spread" 39 | ] 40 | }, 41 | "eslintConfig": { 42 | "env": { 43 | "es6": true, 44 | "browser": true, 45 | "node": true, 46 | "jest": true 47 | }, 48 | "extends": [ 49 | "eslint:recommended" 50 | ], 51 | "parserOptions": { 52 | "ecmaVersion": 2018, 53 | "sourceType": "module" 54 | } 55 | }, 56 | "prettier": { 57 | "trailingComma": "all" 58 | }, 59 | "husky": { 60 | "hooks": { 61 | "pre-commit": "npm run format && npm run lint" 62 | } 63 | }, 64 | "keywords": [ 65 | "animation", 66 | "pixel art", 67 | "generative art", 68 | "procedural art", 69 | "cellular automata" 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /src/__tests__/main.test.js: -------------------------------------------------------------------------------- 1 | import PixelAnimator from "../main"; 2 | 3 | it("should iterate over frames", () => { 4 | const frameIterator = PixelAnimator({ 5 | columns: 2, 6 | rows: 2, 7 | frames: 2, 8 | colorize: ({ column, row, frame, columns, rows, frames }) => ({ 9 | red: column / columns, 10 | green: row / rows, 11 | blue: frame / frames, 12 | }), 13 | }); 14 | const frame1 = frameIterator(); 15 | const frame2 = frameIterator(); 16 | const frame3 = frameIterator(); 17 | expect([frame1, frame2]).toMatchSnapshot(); 18 | expect(frame1).toEqual(frame3); 19 | }); 20 | 21 | it("should maintain state between frames", () => { 22 | const frameIterator = PixelAnimator({ 23 | columns: 2, 24 | rows: 2, 25 | frames: 2, 26 | evolve: ({ column, row, frame, cells }) => { 27 | if (frame === 0) return false; 28 | const self = cells(column, row); 29 | return !self; 30 | }, 31 | colorize: ({ cell }) => (cell ? 1 : 0), 32 | }); 33 | const frame1 = frameIterator(); 34 | const frame2 = frameIterator(); 35 | expect([frame1, frame2]).toMatchSnapshot(); 36 | }); 37 | 38 | it("should wrap when referencing out of bounds cell", () => { 39 | const frameIterator = PixelAnimator({ 40 | columns: 2, 41 | rows: 2, 42 | evolve: ({ column, row, frame, cells }) => { 43 | if (frame === 0) return [column, row]; 44 | return cells(column - 1, row + 1); 45 | }, 46 | colorize: ({ cell }) => { 47 | return { red: cell[0] / 2, green: cell[1] / 2 }; 48 | }, 49 | }); 50 | const frame1 = frameIterator(); 51 | const frame2 = frameIterator(); 52 | expect([frame1, frame2]).toMatchSnapshot(); 53 | }); 54 | 55 | it("should generate HTML when given a DOM node", () => { 56 | document.body.innerHTML = '
'; 57 | const rootElement = document.getElementById("root"); 58 | const transport = PixelAnimator( 59 | { 60 | columns: 2, 61 | rows: 2, 62 | frames: 2, 63 | colorize: ({ column, row, frame, columns, rows, frames }) => ({ 64 | red: column / columns, 65 | green: row / rows, 66 | blue: frame / frames, 67 | }), 68 | }, 69 | rootElement, 70 | ); 71 | transport.pause(); 72 | transport.next(); 73 | expect(rootElement).toMatchSnapshot("first frame"); 74 | transport.next(); 75 | expect(rootElement).toMatchSnapshot("second frame"); 76 | }); 77 | -------------------------------------------------------------------------------- /src/color.js: -------------------------------------------------------------------------------- 1 | const namedColors = { 2 | navy: '001f3f', 3 | blue: '0074D9', 4 | aqua: '7FDBFF', 5 | teal: '39CCCC', 6 | olive: '3D9970', 7 | green: '2ECC40', 8 | lime: '01FF70', 9 | yellow: 'FFDC00', 10 | orange: 'FF851B', 11 | red: 'FF4136', 12 | maroon: '85144b', 13 | fuchsia: 'F012BE', 14 | purple: 'B10DC9', 15 | black: '111111', 16 | gray: 'AAAAAA', 17 | silver: 'DDDDDD', 18 | }; 19 | 20 | function decimalToRgb(decimal) { 21 | return { 22 | red: (decimal >> 16) & 255, 23 | green: (decimal >> 8) & 255, 24 | blue: decimal & 255, 25 | }; 26 | } 27 | 28 | function hexToRgba(hex) { 29 | const decimal = parseInt(hex, 16); 30 | const { red, green, blue } = decimalToRgb(decimal); 31 | return { red: red, green: green, blue: blue, alpha: 1 }; 32 | } 33 | 34 | /** 35 | * Convert a RGB hexadecimal color string like "#FF4136" or human color name 36 | * like "red" to RGBA components in range 0..1. The alpha channel is ignored. 37 | */ 38 | function stringToColor(userString) { 39 | const string = userString.trim().toLowerCase(); 40 | const namedColor = namedColors[string]; 41 | if (namedColor) { 42 | return hexToRgba(namedColor); 43 | } 44 | if (string.indexOf('#') === 0 && string.length > 6) { 45 | return hexToRgba(string.substring(1, 7)); 46 | } 47 | return { red: 0, green: 0, blue: 0, alpha: 1 }; 48 | } 49 | 50 | function scale(number) { 51 | return Math.floor(255 * number); 52 | } 53 | 54 | /* 55 | * Normalize a color given in one of the following formats: 56 | * 57 | * - A string is treated as a hexadecimal RGB color code (no alpha for now). 58 | * - A number is treated as a grayscale value. 59 | * - null is treated as transparent. 60 | * 61 | * Anything else is assumed to be an object containing RGBA components 62 | * in the range 0..1. 63 | */ 64 | export function normalizeColor(color) { 65 | if (typeof color === 'string') { 66 | return stringToColor(color); 67 | } 68 | if (typeof color === 'number') { 69 | return { 70 | red: scale(color), 71 | green: scale(color), 72 | blue: scale(color), 73 | alpha: 1, 74 | }; 75 | } 76 | if (color === null) { 77 | return { red: 0, green: 0, blue: 0, alpha: 0 }; 78 | } 79 | return { 80 | red: color.red ? scale(color.red) : 0, 81 | green: color.green ? scale(color.green) : 0, 82 | blue: color.blue ? scale(color.blue) : 0, 83 | alpha: typeof color.alpha === 'number' ? Math.round(color.alpha) : 1, 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /src/render-dom.js: -------------------------------------------------------------------------------- 1 | import { createFrameIterator } from './create-frame-iterator'; 2 | 3 | /* 4 | * Schedule a callback function for execution on an interval in milliseconds. 5 | * Similar to `setInterval` but can be controlled using the returned transport 6 | * object. `requestAnimationFrame` is meant to prevent calls while the page is 7 | * in the background. 8 | */ 9 | function createLooper(interval, callback) { 10 | let playing; 11 | let timeoutId; 12 | 13 | function loop() { 14 | requestAnimationFrame(() => { 15 | callback(); 16 | timeoutId = setTimeout(loop, interval); 17 | }); 18 | } 19 | 20 | return { 21 | play: () => { 22 | playing = true; 23 | clearTimeout(timeoutId); 24 | loop(); 25 | }, 26 | pause: () => { 27 | playing = false; 28 | clearTimeout(timeoutId); 29 | }, 30 | next: () => { 31 | playing = false; 32 | clearTimeout(timeoutId); 33 | callback(); 34 | }, 35 | playing: () => playing, 36 | }; 37 | } 38 | 39 | function fit(columns, rows, target) { 40 | const scale = target / Math.max(columns, rows); 41 | if (scale >= 1) { 42 | return { 43 | width: Math.floor(columns * scale), 44 | height: Math.floor(rows * scale), 45 | }; 46 | } 47 | return { width: columns, height: rows }; 48 | } 49 | 50 | export function renderAnimatedDom(animation, rootElement) { 51 | rootElement.classList = rootElement.classList + ' pixel-animator'; 52 | const size = fit( 53 | animation.columns, 54 | animation.rows, 55 | rootElement.clientWidth || 320, 56 | ); 57 | rootElement.style.width = size.width + 'px'; 58 | rootElement.style.height = size.height + 'px'; 59 | rootElement.style.fontSize = 0; 60 | const cellWidth = 100 / animation.columns + '%'; 61 | const cellHeight = 100 / animation.rows + '%'; 62 | for (let index = 0; index < animation.rows * animation.columns; index++) { 63 | const cellElement = document.createElement('div'); 64 | cellElement.classList = 'pa-cell ' + 'pa-cell-' + index; 65 | cellElement.style.width = cellWidth; 66 | cellElement.style.height = cellHeight; 67 | cellElement.style.display = 'inline-block'; 68 | cellElement.style.verticalAlign = 'top'; 69 | rootElement.appendChild(cellElement); 70 | } 71 | const frameIterator = createFrameIterator(animation); 72 | const transport = createLooper( 73 | 1000 / animation.frameRate, 74 | function renderNextFrame() { 75 | frameIterator().forEach((color, index) => { 76 | const cellElement = rootElement.children[index]; 77 | cellElement.style.backgroundColor = `rgba(${color.red},${color.green},${color.blue},${color.alpha})`; 78 | }); 79 | }, 80 | ); 81 | transport.play(); 82 | return transport; 83 | } 84 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/main.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should generate HTML when given a DOM node: first frame 1`] = ` 4 |
9 |
13 |
17 |
21 |
25 |
26 | `; 27 | 28 | exports[`should generate HTML when given a DOM node: second frame 1`] = ` 29 |
34 |
38 |
42 |
46 |
50 |
51 | `; 52 | 53 | exports[`should iterate over frames 1`] = ` 54 | Array [ 55 | Array [ 56 | Object { 57 | "alpha": 1, 58 | "blue": 0, 59 | "green": 0, 60 | "red": 0, 61 | }, 62 | Object { 63 | "alpha": 1, 64 | "blue": 0, 65 | "green": 0, 66 | "red": 127, 67 | }, 68 | Object { 69 | "alpha": 1, 70 | "blue": 0, 71 | "green": 127, 72 | "red": 0, 73 | }, 74 | Object { 75 | "alpha": 1, 76 | "blue": 0, 77 | "green": 127, 78 | "red": 127, 79 | }, 80 | ], 81 | Array [ 82 | Object { 83 | "alpha": 1, 84 | "blue": 127, 85 | "green": 0, 86 | "red": 0, 87 | }, 88 | Object { 89 | "alpha": 1, 90 | "blue": 127, 91 | "green": 0, 92 | "red": 127, 93 | }, 94 | Object { 95 | "alpha": 1, 96 | "blue": 127, 97 | "green": 127, 98 | "red": 0, 99 | }, 100 | Object { 101 | "alpha": 1, 102 | "blue": 127, 103 | "green": 127, 104 | "red": 127, 105 | }, 106 | ], 107 | ] 108 | `; 109 | 110 | exports[`should maintain state between frames 1`] = ` 111 | Array [ 112 | Array [ 113 | Object { 114 | "alpha": 1, 115 | "blue": 0, 116 | "green": 0, 117 | "red": 0, 118 | }, 119 | Object { 120 | "alpha": 1, 121 | "blue": 0, 122 | "green": 0, 123 | "red": 0, 124 | }, 125 | Object { 126 | "alpha": 1, 127 | "blue": 0, 128 | "green": 0, 129 | "red": 0, 130 | }, 131 | Object { 132 | "alpha": 1, 133 | "blue": 0, 134 | "green": 0, 135 | "red": 0, 136 | }, 137 | ], 138 | Array [ 139 | Object { 140 | "alpha": 1, 141 | "blue": 255, 142 | "green": 255, 143 | "red": 255, 144 | }, 145 | Object { 146 | "alpha": 1, 147 | "blue": 255, 148 | "green": 255, 149 | "red": 255, 150 | }, 151 | Object { 152 | "alpha": 1, 153 | "blue": 255, 154 | "green": 255, 155 | "red": 255, 156 | }, 157 | Object { 158 | "alpha": 1, 159 | "blue": 255, 160 | "green": 255, 161 | "red": 255, 162 | }, 163 | ], 164 | ] 165 | `; 166 | 167 | exports[`should wrap when referencing out of bounds cell 1`] = ` 168 | Array [ 169 | Array [ 170 | Object { 171 | "alpha": 1, 172 | "blue": 0, 173 | "green": 0, 174 | "red": 0, 175 | }, 176 | Object { 177 | "alpha": 1, 178 | "blue": 0, 179 | "green": 0, 180 | "red": 127, 181 | }, 182 | Object { 183 | "alpha": 1, 184 | "blue": 0, 185 | "green": 127, 186 | "red": 0, 187 | }, 188 | Object { 189 | "alpha": 1, 190 | "blue": 0, 191 | "green": 127, 192 | "red": 127, 193 | }, 194 | ], 195 | Array [ 196 | Object { 197 | "alpha": 1, 198 | "blue": 0, 199 | "green": 127, 200 | "red": 127, 201 | }, 202 | Object { 203 | "alpha": 1, 204 | "blue": 0, 205 | "green": 127, 206 | "red": 0, 207 | }, 208 | Object { 209 | "alpha": 1, 210 | "blue": 0, 211 | "green": 0, 212 | "red": 127, 213 | }, 214 | Object { 215 | "alpha": 1, 216 | "blue": 0, 217 | "green": 0, 218 | "red": 0, 219 | }, 220 | ], 221 | ] 222 | `; 223 | --------------------------------------------------------------------------------