├── .babelrc ├── .gitignore ├── .npmignore ├── .prettierrc ├── README.md ├── assets ├── favicon.ico ├── logo.png └── red_walking_down_1-sheet.png ├── docs ├── getting-started.md └── introduction.md ├── jest.config.js ├── jest.setup.js ├── package.json ├── src ├── components │ ├── Stage.js │ ├── Stage.md │ ├── animation.js │ ├── animation.md │ ├── buffer.js │ ├── buffer.md │ ├── circle.js │ ├── circle.md │ ├── pixel.js │ ├── pixel.md │ ├── rectangle.js │ ├── rectangle.md │ ├── sprite.js │ ├── sprite.md │ ├── text.js │ ├── text.md │ ├── transition.js │ └── transition.md ├── createElement.js ├── easing.js ├── elements │ ├── Animation.js │ ├── Circle.js │ ├── Pixel8Element.js │ ├── PixelBuffer.js │ ├── Rectangle.js │ ├── Sprite.js │ ├── Text.js │ ├── Transition.js │ ├── __snapshots__ │ │ └── index.test.js.snap │ └── index.test.js ├── fonts.js ├── index.js ├── renderer.js ├── transforms.js ├── utils.js └── utils.test.js ├── styleguide.config.js ├── styleguide.setup.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-app"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | lib 4 | main 5 | styleguide 6 | npm-debug.log 7 | yarn-debug.log 8 | yarn-error.log 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | styleguide 4 | npm-debug.log 5 | yarn-debug.log 6 | yarn-error.log 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |

Pixel8

5 |
6 | 7 | 8 | A collection of low-res primitives for creating art and games with React 9 | 10 | [![NPM Version](https://img.shields.io/npm/v/pixel8.svg?style=flat)](https://www.npmjs.org/package/pixel8) 11 | [![NPM Downloads](https://img.shields.io/npm/dm/pixel8.svg?style=flat)](https://www.npmjs.org/package/pixel8) 12 | [![Discord](https://img.shields.io/discord/443995389809393666.svg?style=flat)](https://discord.gg/VYeM6ZK) 13 | 14 | ## Why? 15 | 16 | Pixel8 is my attempt to create a way for developers – beginners and experts alike – to create pixel art and games with a level of simplicity and freedom that I have yet to discover in any alternative. This library does not aim to be a a full game framework or fantasy console, but it can definitely be used as a building block for such apps. 17 | 18 | ## Goals 19 | 20 | ### Easy-to-use 21 | 22 | HTML and Javascript are both incredibly popular languages, and if you know either (or both), [React](https://reactjs.org/) will make you feel at home. Pixel8 has been thoughtfully integrated with its own custom renderer. Because of this, primitives such as `` and `` are built-in and don't need to be imported. Furthermore, JSX makes it easy to describe relatively positioned elements, compose animations, and more. Not to mention, you can still use all of the tools and libraries you do in all your other projects, such as [Redux](https://redux.js.org), [GraphQL](http://graphql.org/), and [Webpack](https://webpack.js.org/). 23 | 24 | ### Performant 25 | 26 | Under the hood, Pixel8 avoids Canvas's stateful/mutable API and relies primarily on `ArrayBuffer`s to render bytes representing pixels directly to a `` `2dContext`. This low-level architecture gives Pixel8 a proper "8-bit" aesthetic, solid performance, and lets future development easily take advantage of new and experimental browser APIs such as `OffscreenCanvas`, `SharedArrayBuffer`, and `WebAssembly`. 27 | 28 | ### Customizable 29 | 30 | As much as possible, Pixel8 doesn't make any assumptions about what you're going for. There are no limitations on color palettes, resolutions, memory/cpu usage, etc. You can make your canvas look like it was created on a ZX Spectrum or a Game Boy. It's entirely up to you. And it's up to the community to develop an ecosystem of tools and libraries that can enforce tasteful constraints for those who wish to opt-in to them. 31 | 32 | ## Installation 33 | 34 | ```bash 35 | yarn add pixel8 36 | # or npm i -s pixel8 37 | ``` 38 | 39 | ## Getting Started 40 | 41 | Definitely check out the interactive documentation at [https://pixel8.vsmode.org/](https://pixel8.vsmode.org/). But if you're looking for a quick start, you probably want to do something like this: 42 | 43 | ```js 44 | import React from 'react' 45 | import { render } from 'react-dom' 46 | import { Stage } from 'pixel8' 47 | 48 | const App = () => ( 49 | 56 | {/* 57 | * Insert your code here! 58 | */} 59 | 60 | ) 61 | 62 | render(, document.getElementById('root')) 63 | 64 | ``` 65 | 66 | ## Issues? Questions? Contributions? 67 | 68 | Feel free to [create an issue](https://github.com/vsmode/pixel8/issues), jump into the [Discord](https://discord.gg/VYeM6ZK), or shoot me a message on [twitter](https://twitter.com/jozanza) 69 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsmode/pixel8/017d202e0480d6ea708cedbaa36067876fd415ae/assets/favicon.ico -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsmode/pixel8/017d202e0480d6ea708cedbaa36067876fd415ae/assets/logo.png -------------------------------------------------------------------------------- /assets/red_walking_down_1-sheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsmode/pixel8/017d202e0480d6ea708cedbaa36067876fd415ae/assets/red_walking_down_1-sheet.png -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | You can install `pixel8` with `yarn` or `npm`: 4 | 5 | ```bash 6 | yarn add @vsmode/pixel8 7 | # or npm install --save @vsmode/pixel8 8 | ``` 9 | 10 |
-------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | ### Pixel8 2 | 3 | > A collection of low-res primitives for creating art and games with React 4 | 5 | If you want to learn how to use it in your project, you may want to skip down to the [Usage](#usage) section. Otherwise, keep reading to learn a little about the background story of this library. 6 | 7 | ## Motivation 8 | 9 | I've been interested in making games and pixel art for a long time. I've dabbled with a number of frameworks like [Phaser](https://phaser.io/), [Game Maker](https://www.yoyogames.com/gamemaker), and [Unity](https://unity3d.com/). While they are all amazing feats of engineering, personally, I found them to be rather heavyweight in their approach – tons of setup, massive APIs, and tutorials/docs that encourage developers to take a very mutative/object-oriented approach to creating games. 10 | 11 | Because of these complexities, I was very excited to learn about the existence of [fantasy consoles](https://medium.com/@G05P3L/fantasy-console-wars-a-guide-to-the-biggest-players-in-retrogamings-newest-trend-56bbe948474d). They are very limited by design, and this approach helps developers learn and build basic games quite easily. However, trying to make any game of scale or do moderately complex things like animations, transitions, or palette swapping can be incredibly challenging to code, test, share, and maintain. 12 | 13 | Pixel8 is my attempt to create a way for developers – beginners and experts alike – to create pixel art and games with a level of simplicity and freedom that I have yet to discover in any alternative. This library does not aim to be a a full game framework or fantasy console, but it can definitely be used as a building block for such apps. 14 | 15 | Anyways, here are some of the main goals I took into consideration when creating Pixel8: 16 | 17 | #### Easy-to-use 18 | 19 | HTML and Javascript are both incredibly popular languages, and if you know either (or both), [React](https://reactjs.org/) will make you feel at home. Pixel8 has been thoughtfully integrated with its own custom renderer. Because of this, primitives such as `` and `` are built-in and don't need to be imported. Furthermore, JSX makes it easy to describe relatively positioned elements, compose animations, and more. Not to mention, you can still use all of the tools and libraries you do in all your other projects, such as [Redux](https://redux.js.org), [GraphQL](http://graphql.org/), and [Webpack](https://webpack.js.org/). 20 | 21 | #### Performant 22 | 23 | Under the hood, Pixel8 avoids Canvas's stateful/mutable API and relies primarily on `ArrayBuffer`s to render bytes representing pixels directly to a `` `2dContext`. This low-level architecture gives Pixel8 a proper "8-bit" aesthetic, solid performance, and lets future development easily take advantage of new and experimental browser APIs such as `OffscreenCanvas`, `SharedArrayBuffer`, and `WebAssembly`. 24 | 25 | #### Customizable 26 | 27 | As much as possible, Pixel8 doesn't make any assumptions about what you're going for. There are no limitations on color palettes, resolutions, memory/cpu usage, etc. You can make your canvas look like it was created on a ZX Spectrum or a Game Boy. It's entirely up to you. And it's up to the community to develop an ecosystem of tools and libraries that can enforce tasteful constraints for those who wish to opt-in to them. 28 | 29 |
-------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFiles: ['./jest.setup.js'], 3 | snapshotSerializers: ['enzyme-to-json/serializer'], 4 | } 5 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import 'raf/polyfill' 2 | 3 | import Enzyme from 'enzyme' 4 | import Adapter from 'enzyme-adapter-react-16' 5 | 6 | Enzyme.configure({ adapter: new Adapter() }) 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixel8", 3 | "version": "0.2.3", 4 | "main": "main/index.js", 5 | "module": "lib/index.js", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/vsmode/pixel8.git" 10 | }, 11 | "dependencies": { 12 | "prop-types": "^15.6.0", 13 | "react": "^16.1.0", 14 | "react-auto-scale": "^1.0.4", 15 | "react-dom": "^16.1.0", 16 | "react-reconciler": "^0.6.0", 17 | "recompose": "^0.26.0", 18 | "styled-components": "^2.2.3" 19 | }, 20 | "scripts": { 21 | "build": "npm -s run build-module && npm -s run build-main", 22 | "build-main": "NODE_ENV=production npx babel src --out-dir main --ignore spec.js,test.js --plugins=transform-es2015-modules-commonjs --source-maps inline", 23 | "build-module": "NODE_ENV=production npx babel src --out-dir lib --ignore spec.js,test.js --source-maps inline", 24 | "prebuild": "rm -rf lib && rm -rf main", 25 | "prepare": "npm -s run build", 26 | "test": "npx jest", 27 | "start": "npm -s run styleguide", 28 | "styleguide": "npx styleguidist server", 29 | "styleguide:build": "npx styleguidist build" 30 | }, 31 | "devDependencies": { 32 | "babel-cli": "^6.26.0", 33 | "babel-preset-react-app": "^3.1.0", 34 | "enzyme": "^3.1.1", 35 | "enzyme-adapter-react-16": "^1.1.0", 36 | "enzyme-to-json": "^3.2.2", 37 | "jest": "^21.2.1", 38 | "raf": "^3.4.0", 39 | "react-addons-test-utils": "^15.6.2", 40 | "react-styleguidist": "^6.0.33", 41 | "react-test-renderer": "^16.1.0", 42 | "webpack": "^3.8.1", 43 | "webpack-blocks": "^1.0.0-rc.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/Stage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled from 'styled-components' 4 | import AutoScale from 'react-auto-scale' 5 | import createRenderer from '../renderer' 6 | import createElement from '../createElement' 7 | import { clickToCoords } from '../utils' 8 | const { render, unmount } = createRenderer(createElement) 9 | 10 | const StageBackground = styled.div` 11 | pointer-events: none; 12 | position: absolute; 13 | top: 0; 14 | bottom: 0; 15 | left: 0; 16 | right: 0; 17 | background-color: ${({ bgColor }) => bgColor}; 18 | ${({ gridType, gridColor, gridSize }) => { 19 | if (!gridType || gridType === 'none') return '' 20 | if (gridType === 'dotted') 21 | return ` 22 | background-position: 0 0; 23 | background-image: 24 | url('data:image/svg+xml;utf8, 25 | 30 | 36 | ');`.replace(/\n/g, ' ') 37 | if (gridType === 'checkered') 38 | return ` 39 | background-position: 0 0; 40 | background-image: 41 | url('data:image/svg+xml;utf8, 42 | 47 | 53 | 59 | ');`.replace(/\n/g, ' ') 60 | }}; 61 | ` 62 | 63 | /** 64 | * ## `` 65 | * The magical gateway into the world of pixel awesomeness (づ ̄ ³ ̄)づ 66 | */ 67 | export default class Stage extends Component { 68 | static propTypes = { 69 | /** Stage width in pixels */ 70 | width: PropTypes.number, 71 | /** Stage height in pixels */ 72 | height: PropTypes.number, 73 | /** Stage scale. Basically, the zoom level */ 74 | scale: PropTypes.number, 75 | /** Stage background color */ 76 | background: PropTypes.string, 77 | /** Distance between grid dots on the stage. If `0`, then no dots appear */ 78 | gridSize: PropTypes.number, 79 | /** Stage grid color */ 80 | gridColor: PropTypes.string, 81 | /** The grid style */ 82 | gridType: PropTypes.oneOf(['checkered', 'dotted']), 83 | /** How often to redraw the stage (frames per second) */ 84 | fps: PropTypes.number, 85 | /** Called once after initialization, before the first draw/tick 86 | * 87 | * `function onInit(stage: pixel8.Stage): void` */ 88 | onInit: PropTypes.func, 89 | /** Called every tick (frame) 90 | * 91 | * `function onTick(stage: pixel8.Stage): void` */ 92 | onTick: PropTypes.func, 93 | /** Called right after the stage pixel buffer gets redrawn 94 | * 95 | * `function onDraw(stage: pixel8.Stage): void` */ 96 | onDraw: PropTypes.func, 97 | } 98 | static defaultProps = { 99 | width: 128, 100 | height: 128, 101 | scale: 1, 102 | background: 'transparent', 103 | gridSize: 2, // scaled pixels 104 | gridType: 'checkered', 105 | gridColor: 'transparent', 106 | fps: 0, 107 | onInit: () => {}, 108 | onTick: () => {}, 109 | onDraw: () => {}, 110 | onClick: () => {}, 111 | } 112 | tick = 0 113 | children = new Set() 114 | childMap = new Map() 115 | appendChild = child => { 116 | this.children.add(child) 117 | this.registerChild(child) 118 | } 119 | removeChild = child => { 120 | this.children.delete(child) 121 | this.unregisterChild(child) 122 | } 123 | registerChild = child => { 124 | this.childMap.set(child.id, child) 125 | } 126 | unregisterChild = child => { 127 | this.childMap.delete(child.id) 128 | } 129 | init() { 130 | const { width, height } = this.props 131 | const size = width * height * 4 132 | this.ctx = this.canvas.getContext('2d') 133 | this.ctx.globalAlpha = 0 134 | // visible pixels 135 | this.pixelBuf = new ArrayBuffer(size) 136 | this.pixels = new Uint32Array(this.pixelBuf) 137 | // collision pixels 138 | this.hitBuf = new ArrayBuffer(size) 139 | this.hitmap = new Uint32Array(this.hitBuf) 140 | // create ImageData 141 | this.imageData = new ImageData( 142 | new Uint8ClampedArray(this.pixelBuf), 143 | width, 144 | height, 145 | ) 146 | // draw + update loop 147 | this.timer = requestInterval(() => { 148 | this.props.onTick(this) 149 | this.tick++ 150 | this.draw() 151 | }, 1 / this.props.fps * 1000) 152 | // onInit() callback 153 | this.props.onInit(this) 154 | } 155 | // utils methods 156 | getPixel = (x, y) => this.pixels[y * this.props.width + x] 157 | getChild = (x, y) => this.childMap.get(this.hitmap[y * this.props.width + x]) 158 | componentDidMount() { 159 | this.init() 160 | render(this) 161 | this.draw() 162 | } 163 | componentWillUnmount() { 164 | cancelInterval(this.timer) 165 | unmount(this) 166 | } 167 | componentDidUpdate() { 168 | render(this) 169 | } 170 | draw() { 171 | // clear pixels and hitmap 172 | for (let i = 0; i < this.pixels.length; i++) { 173 | this.pixels[i] = this.hitmap[i] = 0 174 | } 175 | // update pixels 176 | for (const child of this.children) { 177 | child.draw() 178 | } 179 | // draw pixels to the canvas 180 | this.ctx.putImageData(this.imageData, 0, 0) 181 | this.props.onDraw(this) 182 | } 183 | render() { 184 | const { 185 | fps, 186 | children, 187 | background, 188 | gridType, 189 | gridSize, 190 | gridColor, 191 | palette, 192 | width, 193 | height, 194 | scale, 195 | onInit, 196 | onDraw, 197 | onTick, 198 | onClick, 199 | ...props 200 | } = this.props 201 | const maxWidth = width * scale 202 | const maxHeight = height * scale 203 | return ( 204 |
211 | 216 |
224 | 230 | (this.canvas = ref)} 232 | width={width} 233 | height={height} 234 | style={{ 235 | position: 'absolute', 236 | top: 0, 237 | left: 0, 238 | imageRendering: 'pixelated', 239 | transform: `scale(${scale})`, 240 | transformOrigin: '0 0', 241 | outline: 'none', 242 | }} 243 | onClick={e => { 244 | e.persist() 245 | onClick(e) 246 | const { x, y } = clickToCoords(e, scale, maxWidth, maxHeight) 247 | const child = this.getChild(x, y) 248 | if (child && child.props.onClick) { 249 | child.props.onClick(e) 250 | } 251 | }} 252 | {...props} 253 | /> 254 |
255 |
256 |
257 | ) 258 | } 259 | } 260 | 261 | export const requestInterval = (cb, ms) => { 262 | const self = {} 263 | let start = performance.now() 264 | const updateCancel = x => { 265 | cancelInterval.intervals.set(self, x) 266 | } 267 | const next = () => { 268 | if (performance.now() - start >= ms) { 269 | start += ms 270 | cb() 271 | } 272 | updateCancel(requestAnimationFrame(next)) 273 | } 274 | updateCancel(requestAnimationFrame(next)) 275 | return self 276 | } 277 | export const cancelInterval = (() => { 278 | const cancelInterval = self => { 279 | const x = cancelInterval.intervals.get(self) 280 | cancelAnimationFrame(x) 281 | } 282 | cancelInterval.intervals = new WeakMap() 283 | return cancelInterval 284 | })() 285 | -------------------------------------------------------------------------------- /src/components/Stage.md: -------------------------------------------------------------------------------- 1 | ```js 2 |
3 | 6 | 11 | 17 | 25 | 34 |
35 | ``` -------------------------------------------------------------------------------- /src/components/animation.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | /** 5 | * ## `` 6 | */ 7 | export default class animation extends React.Component { 8 | static propTypes = { 9 | /** 10 | * the animation function 11 | * `(frame: number, props: Props) => Props` 12 | */ 13 | use: PropTypes.func.isRequired, 14 | /** frames per loop (duration) */ 15 | frames: PropTypes.number, 16 | /** number of frames to delay before starting each loop */ 17 | delay: PropTypes.number, 18 | /** number of times to run animation loop (iteration count) */ 19 | loops: PropTypes.number, 20 | /** if `true`, runs animation loop in reverse every other loop */ 21 | alternate: PropTypes.bool, 22 | /** easing function (linear by default) 23 | * 24 | * you may provide a timing function `(t => t)` or 25 | * 26 | * the name of one of the built-in easing functions: 27 | * 28 | * `linear` 29 | * `easeInQuad` `easeOutQuad` `easeInOutQuad` 30 | * `easeInCubic` `easeOutCubic` `easeInOutCubic` 31 | * `easeInQuart` `easeOutQuart` `easeInOutQuart` 32 | * `easeInQuint` `easeOutQuint` `easeInOutQuint` 33 | * 34 | *
35 | */ 36 | ease: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), 37 | } 38 | static defaultProps = { 39 | frames: 8, 40 | delay: 0, 41 | loops: Infinity, 42 | alternate: true, 43 | ease: t => t, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/animation.md: -------------------------------------------------------------------------------- 1 | ```js 2 | const animations = { 3 | moveDown: (n, props) => ({ 4 | ...props, 5 | y: props.y + n 6 | }), 7 | moveRight: (n, props) => ({ 8 | ...props, 9 | x: props.x + n 10 | }) 11 | }; 12 | 19 | {/* 20 | * ~(˘▾˘~) composable animations with easing (~˘▾˘)~ 21 | */} 22 | 29 | 36 | 44 | 45 | 46 | {/* 47 | * with a delay 48 | */} 49 | 55 | 63 | 64 | {/* 65 | * sequential animations 66 | */} 67 | { 69 | const n = 60 70 | return t <= n 71 | ? animations.moveRight(t, props) 72 | : animations.moveDown(t - n, animations.moveRight(n, props)) 73 | }} 74 | frames={80} 75 | alternate={true}> 76 | 84 | 85 | 86 | ``` -------------------------------------------------------------------------------- /src/components/buffer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | /** 5 | * ## `` 6 | * Draws an array of raw bytes to the stage 7 | */ 8 | export default class buffer extends React.Component { 9 | static propTypes = { 10 | /** x coordinate */ 11 | x: PropTypes.number, 12 | /** y coordinate */ 13 | y: PropTypes.number, 14 | /** width */ 15 | width: PropTypes.number.isRequired, 16 | /** height */ 17 | height: PropTypes.number.isRequired, 18 | /** TypedArray, ArrayBuffer or Array with unsigned bytes to render to the canvas */ 19 | data: PropTypes.oneOfType([ 20 | PropTypes.instanceOf(Array), 21 | PropTypes.instanceOf(ArrayBuffer), 22 | PropTypes.instanceOf(Uint32Array), 23 | PropTypes.instanceOf(Uint16Array), 24 | PropTypes.instanceOf(Uint8Array), 25 | PropTypes.instanceOf(Uint8ClampedArray), 26 | ]), 27 | /** click handler (transparent pixels are ignored) */ 28 | onClick: PropTypes.func, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/buffer.md: -------------------------------------------------------------------------------- 1 | If you can't find a good way to accomplish what you're attempting to do with the other built-in components, `` might be what you're looking for. 2 | 3 | ```js 4 | const { toUint32 } = pixel8.utils; 5 | 11 | 42 | { 55 | // can be used for palette swapping 56 | switch (byte) { 57 | case 1: return toUint32('rgba(0, 0, 0, 1)') 58 | case 2: return toUint32('rgba(0, 0, 0, .75)') 59 | case 3: return toUint32('rgba(0, 0, 0, .25)') 60 | default: return byte 61 | } 62 | })} 63 | /> 64 | 65 | ``` -------------------------------------------------------------------------------- /src/components/circle.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | /** 5 | * ## `` 6 | */ 7 | export default class circle extends React.Component { 8 | static propTypes = { 9 | /** x position */ 10 | x: PropTypes.number, 11 | /** y position */ 12 | y: PropTypes.number, 13 | /** radius */ 14 | radius: PropTypes.number, 15 | /** color represented by a 16 | * - 32-bit unsigned integer (default `0` === transparent) 17 | * - hex string 18 | * - rgb/rgba string 19 | */ 20 | fill: PropTypes.oneOfType([ 21 | PropTypes.string, 22 | PropTypes.number, 23 | ]), 24 | /** click handler (transparent pixels are ignored) */ 25 | onClick: PropTypes.func, 26 | } 27 | static defaultProps = { 28 | x: 0, 29 | y: 0, 30 | radius: 0, 31 | fill: 0, 32 | } 33 | } -------------------------------------------------------------------------------- /src/components/circle.md: -------------------------------------------------------------------------------- 1 | ```js 2 | 9 | 15 | 21 | {/* face made with nested circles */} 22 | 27 | {/* ears */} 28 | 34 | 40 | 46 | 52 | {/* eyes */} 53 | 59 | 65 | 71 | 77 | {/* nose */} 78 | 84 | {/* mouth */} 85 | 91 | 92 | 93 | ``` 94 | 95 |
-------------------------------------------------------------------------------- /src/components/pixel.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | /** 5 | * ## `` 6 | */ 7 | export default class pixel extends React.Component { 8 | static propTypes = { 9 | /** x position */ 10 | x: PropTypes.number, 11 | /** y position */ 12 | y: PropTypes.number, 13 | /** color represented by a 14 | * - 32-bit unsigned integer (default `0` === transparent) 15 | * - hex string 16 | * - rgb/rgba string 17 | **/ 18 | fill: PropTypes.oneOfType([ 19 | PropTypes.string, 20 | PropTypes.number, 21 | ]), 22 | /** click handler (transparent pixels are ignored) */ 23 | onClick: PropTypes.func, 24 | } 25 | static defaultProps = { 26 | x: 0, 27 | y: 0, 28 | fill: 0, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/pixel.md: -------------------------------------------------------------------------------- 1 | Let's create some 1-bit art: 2 | 3 | ```js 4 |
5 | {/* 6 | * 1-bit smiley 7 | */} 8 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {/* 31 | * 1-bit heart 32 | */} 33 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | {/* 66 | * 1-bit sword 67 | */} 68 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 |
95 | ``` 96 | 97 | Looking good! Hand-coding those pixels was pretty exhausting. How about we map over the pixels instead and do a cool time-lapse thing to spice it up a bit? 98 | 99 | ```js 100 | initialState = { 101 | pixels: [ 102 | { x: 6, y: 0 }, 103 | { x: 7, y: 0 }, 104 | { x: 5, y: 1 }, 105 | { x: 6, y: 1 }, 106 | { x: 7, y: 1 }, 107 | { x: 2, y: 2 }, 108 | { x: 4, y: 2 }, 109 | { x: 5, y: 2 }, 110 | { x: 6, y: 2 }, 111 | { x: 2, y: 3 }, 112 | { x: 3, y: 3 }, 113 | { x: 5, y: 3 }, 114 | { x: 3, y: 4 }, 115 | { x: 4, y: 4 }, 116 | { x: 2, y: 5 }, 117 | { x: 4, y: 5 }, 118 | { x: 5, y: 5 }, 119 | { x: 1, y: 6 }, 120 | ], 121 | to: 0 122 | }; 123 | { 131 | setState({ to: (state.to + 1) % state.pixels.length + 1 }) 132 | }}> 133 | {state.pixels.slice(0, state.to).map(({ x, y }, i) => 134 | 135 | )} 136 | 137 | ``` 138 | 139 |
-------------------------------------------------------------------------------- /src/components/rectangle.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | /** 5 | * ## `` 6 | */ 7 | export default class rectangle extends React.Component { 8 | static propTypes = { 9 | /** x position */ 10 | x: PropTypes.number, 11 | /** y position */ 12 | y: PropTypes.number, 13 | /** width */ 14 | width: PropTypes.number, 15 | /** height */ 16 | height: PropTypes.number, 17 | /** border radius */ 18 | borderRadius: PropTypes.number, 19 | /** color represented by a 20 | * - 32-bit unsigned integer (default `0` === transparent) 21 | * - hex string 22 | * - rgb/rgba string 23 | */ 24 | fill: PropTypes.oneOfType([ 25 | PropTypes.string, 26 | PropTypes.number, 27 | ]), 28 | /** click handler (transparent pixels are ignored) */ 29 | onClick: PropTypes.func, 30 | } 31 | static defaultProps = { 32 | x: 0, 33 | y: 0, 34 | width: 0, 35 | height: 0, 36 | borderRadius: 0, 37 | fill: 0, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/rectangle.md: -------------------------------------------------------------------------------- 1 | ```js 2 | 9 | 17 | 25 | {/* robot */} 26 | 33 | {/* eyes */} 34 | 42 | 50 | {/* "ears" */} 51 | 59 | 67 | 75 | 83 | {/* mouth */} 84 | 92 | 93 | 94 | ``` 95 | 96 |
-------------------------------------------------------------------------------- /src/components/sprite.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | /** 5 | * ## `` 6 | */ 7 | export default class sprite extends React.Component { 8 | static propTypes = { 9 | /** x coordinate */ 10 | x: PropTypes.number, 11 | /** y coordinate */ 12 | y: PropTypes.number, 13 | /** width */ 14 | width: PropTypes.number.isRequired, 15 | /** height */ 16 | height: PropTypes.number.isRequired, 17 | /** color represented by a 18 | * - 32-bit unsigned integer (default `0` === transparent) 19 | * - hex string 20 | * - rgb/rgba string 21 | */ 22 | fill: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 23 | /** click handler (transparent pixels are ignored) */ 24 | onClick: PropTypes.func, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/sprite.md: -------------------------------------------------------------------------------- 1 | `` makes it simple to get a sub-sprite from a larger spritesheet. The only caveat here is that it expects the spritesheet to be the width of a single sprite -- like a filmstrip of sorts. The `index` prop will adjust the vertical offset that is rendered. Combining `` with ``, you can quickly throw together an animated sprite from a single image. Check out the example below: 2 | 3 | ```js 4 | initialState = { source: null }; 5 | const indexByFrame = frames => (t, props) => { 6 | return { 7 | ...props, 8 | index: frames[t] 9 | } 10 | }; 11 |
12 | { 20 | // Load the sprite as ImageData 21 | pixel8.utils 22 | .loadImageData('/red_walking_down_1-sheet.png') 23 | .then(source => setState({ source })) 24 | }}> 25 | {/* first frame */} 26 | {state.source && ( 27 | 35 | )} 36 | {/* second frame */} 37 | {state.source && ( 38 | 46 | )} 47 | {/* animated */} 48 | {state.source && ( 49 | 52 | 59 | 60 | )} 61 | {/* animated + palette shifted */} 62 | {state.source && ( 63 | 71 | blue 79 | ['#d95763', '#29adff'], 80 | // white -> brown 81 | ['#ffffff', '#ab5236'], 82 | ])} 83 | /> 84 | 85 | )} 86 | 87 |
88 | original image: 89 |
90 |
91 | ``` -------------------------------------------------------------------------------- /src/components/text.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | /** 5 | * ## `` 6 | */ 7 | export default class text extends React.Component { 8 | static propTypes = { 9 | /** x coordinate */ 10 | x: PropTypes.number, 11 | /** y coordinate */ 12 | y: PropTypes.number, 13 | /** width */ 14 | width: PropTypes.number.isRequired, 15 | /** height */ 16 | height: PropTypes.number.isRequired, 17 | /** text */ 18 | text: PropTypes.string, 19 | /** line-Height (leading)*/ 20 | lineHeight: PropTypes.number, 21 | /** yOffset. Useful for scrolling */ 22 | yOffset: PropTypes.number, 23 | /** color represented by a 24 | * - 32-bit unsigned integer (default `0` === transparent) 25 | * - hex string 26 | * - rgb/rgba string 27 | */ 28 | fill: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 29 | /** object containing required font information: */ 30 | font: PropTypes.shape({ 31 | /** width of each char */ 32 | width: PropTypes.number, 33 | /** height of each char */ 34 | height: PropTypes.number, 35 | /** tile index of each char */ 36 | charmap: PropTypes.objectOf(PropTypes.number), 37 | /** font bits (lots of 1s and 0s) */ 38 | data: PropTypes.arrayOf(PropTypes.number), 39 | }), 40 | /** click handler (transparent pixels are ignored) */ 41 | onClick: PropTypes.func, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/text.md: -------------------------------------------------------------------------------- 1 | ```js 2 | initialState = { yOffset: 0, clientY: 0 }; 3 | const w = 256; 4 | const h = 256; 5 | const lineHeight = 10; 6 | const text = 'Protip:\nScroll with your trackpad or mousewheel\n...\n..\n.'.toUpperCase() + ('\n\nSweet candy canes liquorice ice cream topping cheesecake biscuit fruitcake. Tiramisu sesame snaps chocolate bar ice cream candy. Cake croissant liquorice. Biscuit fruitcake sweet roll fruitcake pastry. Dessert biscuit macaroon muffin tart liquorice marzipan tootsie roll. Biscuit croissant carrot cake bonbon marzipan jujubes gummi bears cake. Souffle pastry candy canes. Muffin chocolate lollipop cotton candy carrot cake chocolate bar candy. Jelly beans chupa chups bear claw jelly beans fruitcake tiramisu.'.toUpperCase().repeat(20)); 7 | const lines = pixel8.utils.stringToLines(text.split(/[ ]/), pixel8.fonts.micro.width, w - 10); 8 | const MAX_YOFFSET = 0; 9 | const MIN_YOFFSET = (lines.length - (1 + Math.floor((h - 10) / lineHeight))) * -lineHeight; 10 | const scrollY = 1 + Math.round((h - 17) * (state.yOffset / MIN_YOFFSET)); 11 | let prevClientY = 0; 12 | { 20 | let yOffset = state.yOffset - e.deltaY 21 | if (MAX_YOFFSET < yOffset) yOffset = MAX_YOFFSET 22 | else if (MIN_YOFFSET > yOffset) yOffset = MIN_YOFFSET 23 | else e.preventDefault() 24 | setState({ yOffset }) 25 | }} 26 | // touch device scrolling 27 | onTouchStart={e => { 28 | const clientY = Math.round(e.touches[0].clientY) 29 | setState({ clientY }) 30 | }} 31 | onTouchMove={e => { 32 | const clientY = Math.round(e.changedTouches[0].clientY) 33 | const deltaY = -1 * (clientY - state.clientY) 34 | let yOffset = state.yOffset - deltaY 35 | if (MAX_YOFFSET < yOffset) yOffset = MAX_YOFFSET 36 | else if (MIN_YOFFSET > yOffset) yOffset = MIN_YOFFSET 37 | else e.preventDefault() 38 | setState({ yOffset, clientY }) 39 | }}> 40 | {/* draw text at yOffset */} 41 | 51 | {/* scrollbar */} 52 | 60 | 61 | ``` -------------------------------------------------------------------------------- /src/components/transition.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | /** 5 | * ## `` 6 | */ 7 | export default class transition extends React.Component { 8 | static propTypes = { 9 | /** 10 | * the transition function 11 | * `(from: Props, to: Props, t: number) => Props` 12 | */ 13 | use: PropTypes.func, 14 | /** frames per transition (duration) */ 15 | frames: PropTypes.number, 16 | /** number of frames to delay before starting transition */ 17 | delay: PropTypes.number, 18 | /** easing function (linear by default) 19 | * 20 | * you may provide a timing function `(t => t)` or 21 | * 22 | * the name of one of the built-in easing functions: 23 | * 24 | * `linear` 25 | * `easeInQuad` `easeOutQuad` `easeInOutQuad` 26 | * `easeInCubic` `easeOutCubic` `easeInOutCubic` 27 | * `easeInQuart` `easeOutQuart` `easeInOutQuart` 28 | * `easeInQuint` `easeOutQuint` `easeInOutQuint` 29 | * 30 | *
31 | */ 32 | ease: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), 33 | /** called when a transition completes */ 34 | onTransitionEnd: PropTypes.func, 35 | } 36 | static defaultProps = { 37 | frames: 8, 38 | delay: 0, 39 | ease: t => t, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/transition.md: -------------------------------------------------------------------------------- 1 | ```js 2 | initialState = { x: 30, y: 30, width: 4, height: 4, borderRadius: 0, delay: 0 }; 3 |
4 | 11 | { 16 | console.log('TRANSITIONED!') 17 | }}> 18 | 26 | 27 | 28 |
29 | 30 |