├── .npmignore ├── __mocks__ └── fileMock.js ├── test-config ├── shim.js └── test-setup.js ├── examples ├── shared │ ├── img │ │ ├── 1.jpg │ │ ├── 10.jpg │ │ ├── 11.jpg │ │ ├── 2.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── 5.jpg │ │ ├── 6.jpg │ │ ├── 7.jpg │ │ ├── 8.jpg │ │ ├── 9.jpg │ │ ├── ink.png │ │ ├── canvas.png │ │ ├── wireframe.png │ │ ├── Dribbble │ │ │ ├── 1.png │ │ │ ├── 10.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ ├── 4.png │ │ │ ├── 5.png │ │ │ ├── 6.png │ │ │ ├── 7.png │ │ │ ├── 8.png │ │ │ └── 9.png │ │ ├── Dribbble1 │ │ │ ├── 1.jpg │ │ │ ├── 2.jpg │ │ │ ├── 3.jpg │ │ │ ├── 4.jpg │ │ │ ├── 1_1.jpg │ │ │ ├── 1_2.jpg │ │ │ ├── 1_3.jpg │ │ │ ├── 2_1.jpg │ │ │ ├── 2_2.jpg │ │ │ ├── 3_1.jpg │ │ │ ├── 3_2.jpg │ │ │ ├── 4_1.jpg │ │ │ └── 4_2.jpg │ │ ├── related │ │ │ ├── ImageGridEffects.jpg │ │ │ └── GridItemAnimation.jpg │ │ ├── menu_icon.svg │ │ └── PageCloud_logo.svg │ └── favicon │ │ └── favicon.ico ├── storybooks │ ├── storyshots.test.js │ ├── generic.scss │ ├── basic-stack.js │ ├── index.js │ ├── side-stack.js │ ├── sandbox-utils.js │ ├── basic-stack-with-title.js │ ├── loaded-callback.js │ ├── spread.js │ └── __snapshots__ │ │ └── storyshots.test.js.snap └── basic-example │ ├── index.html │ ├── index.js │ ├── stylesheets │ ├── app.scss │ └── vendor │ │ ├── github-light.css │ │ └── stylesheet.css │ └── app.js ├── src ├── index.js ├── utils │ ├── misc.js │ └── animation-frame.js ├── react-isometric-grid.scss ├── cell.js ├── react-isometric-grid.js └── isometric-grid.js ├── .storybook ├── addons.js ├── config.js └── webpack.config.js ├── .eslintrc.json ├── .gitignore ├── .codeclimate.yml ├── .babelrc ├── .prettierrc ├── .travis.yml ├── LICENSE ├── CODE_OF_CONDUCT.md ├── webpack.config.js ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | lib 3 | CODE_OF_CONDUCT.md 4 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /test-config/shim.js: -------------------------------------------------------------------------------- 1 | global.requestAnimationFrame = callback => { 2 | setTimeout(callback, 0); 3 | }; 4 | -------------------------------------------------------------------------------- /examples/shared/img/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/1.jpg -------------------------------------------------------------------------------- /examples/shared/img/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/10.jpg -------------------------------------------------------------------------------- /examples/shared/img/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/11.jpg -------------------------------------------------------------------------------- /examples/shared/img/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/2.jpg -------------------------------------------------------------------------------- /examples/shared/img/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/3.jpg -------------------------------------------------------------------------------- /examples/shared/img/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/4.jpg -------------------------------------------------------------------------------- /examples/shared/img/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/5.jpg -------------------------------------------------------------------------------- /examples/shared/img/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/6.jpg -------------------------------------------------------------------------------- /examples/shared/img/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/7.jpg -------------------------------------------------------------------------------- /examples/shared/img/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/8.jpg -------------------------------------------------------------------------------- /examples/shared/img/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/9.jpg -------------------------------------------------------------------------------- /examples/shared/img/ink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/ink.png -------------------------------------------------------------------------------- /examples/shared/img/canvas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/canvas.png -------------------------------------------------------------------------------- /examples/shared/img/wireframe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/wireframe.png -------------------------------------------------------------------------------- /examples/shared/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/favicon/favicon.ico -------------------------------------------------------------------------------- /examples/shared/img/Dribbble/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble/1.png -------------------------------------------------------------------------------- /examples/shared/img/Dribbble/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble/10.png -------------------------------------------------------------------------------- /examples/shared/img/Dribbble/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble/2.png -------------------------------------------------------------------------------- /examples/shared/img/Dribbble/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble/3.png -------------------------------------------------------------------------------- /examples/shared/img/Dribbble/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble/4.png -------------------------------------------------------------------------------- /examples/shared/img/Dribbble/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble/5.png -------------------------------------------------------------------------------- /examples/shared/img/Dribbble/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble/6.png -------------------------------------------------------------------------------- /examples/shared/img/Dribbble/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble/7.png -------------------------------------------------------------------------------- /examples/shared/img/Dribbble/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble/8.png -------------------------------------------------------------------------------- /examples/shared/img/Dribbble/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble/9.png -------------------------------------------------------------------------------- /examples/shared/img/Dribbble1/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble1/1.jpg -------------------------------------------------------------------------------- /examples/shared/img/Dribbble1/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble1/2.jpg -------------------------------------------------------------------------------- /examples/shared/img/Dribbble1/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble1/3.jpg -------------------------------------------------------------------------------- /examples/shared/img/Dribbble1/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble1/4.jpg -------------------------------------------------------------------------------- /examples/shared/img/Dribbble1/1_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble1/1_1.jpg -------------------------------------------------------------------------------- /examples/shared/img/Dribbble1/1_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble1/1_2.jpg -------------------------------------------------------------------------------- /examples/shared/img/Dribbble1/1_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble1/1_3.jpg -------------------------------------------------------------------------------- /examples/shared/img/Dribbble1/2_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble1/2_1.jpg -------------------------------------------------------------------------------- /examples/shared/img/Dribbble1/2_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble1/2_2.jpg -------------------------------------------------------------------------------- /examples/shared/img/Dribbble1/3_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble1/3_1.jpg -------------------------------------------------------------------------------- /examples/shared/img/Dribbble1/3_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble1/3_2.jpg -------------------------------------------------------------------------------- /examples/shared/img/Dribbble1/4_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble1/4_1.jpg -------------------------------------------------------------------------------- /examples/shared/img/Dribbble1/4_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/Dribbble1/4_2.jpg -------------------------------------------------------------------------------- /test-config/test-setup.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /examples/shared/img/related/ImageGridEffects.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/related/ImageGridEffects.jpg -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ReactIsometricGrid from './react-isometric-grid'; 2 | import Cell from './cell'; 3 | 4 | export default ReactIsometricGrid; 5 | export { Cell }; 6 | -------------------------------------------------------------------------------- /examples/shared/img/related/GridItemAnimation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontend-collective/react-isometric-grid/HEAD/examples/shared/img/related/GridItemAnimation.jpg -------------------------------------------------------------------------------- /examples/storybooks/storyshots.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import initStoryshots from '@storybook/addon-storyshots'; 3 | 4 | initStoryshots(); 5 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import '@storybook/addon-options/register'; 3 | import '@storybook/addon-notes/register'; 4 | import '@storybook/addon-actions/register'; 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "prettier", "prettier/react"], 3 | "env": { 4 | "browser": true, 5 | "jest": true 6 | }, 7 | "rules": { 8 | "react/jsx-filename-extension": 0, 9 | "react/prefer-stateless-function": 0 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | yarn.lock 4 | coverage 5 | cc-test-reporter 6 | 7 | # Editor and other tmp files 8 | *.swp 9 | *.un~ 10 | *.iml 11 | *.ipr 12 | *.iws 13 | *.sublime-* 14 | .idea/ 15 | *.DS_Store 16 | 17 | # Build directories (Will be preserved by npm) 18 | dist 19 | build 20 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | eslint: 3 | enabled: true 4 | checks: 5 | import/extensions: 6 | enabled: false 7 | ratings: 8 | paths: 9 | - src/** 10 | - examples/** 11 | - "**.js" 12 | exclude_paths: 13 | - "**/*.test.js" 14 | - "node_modules/" 15 | - "__mocks__/" 16 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "browsers": ["last 2 versions", "ie >= 9"] 8 | } 9 | } 10 | ], 11 | "react" 12 | ], 13 | "plugins": [ 14 | "transform-class-properties", 15 | "transform-object-rest-spread", 16 | "react-hot-loader/babel" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /examples/shared/img/menu_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | menu_icon 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/basic-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Isometric Grid 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "overrides": [ 11 | { 12 | "files": ".prettierrc", 13 | "options": { "parser": "json", "trailingComma": "none" } 14 | }, 15 | { 16 | "files": "*.json", 17 | "options": { "trailingComma": "none" } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { configure } from '@storybook/react'; 3 | import { setOptions } from '@storybook/addon-options'; 4 | 5 | setOptions({ 6 | name: 'React Isometric Grid', 7 | url: 'https://github.com/wuweiweiwu/react-isometric-grid', 8 | }); 9 | 10 | function loadStories() { 11 | // eslint-disable-next-line global-require 12 | require('../examples/storybooks'); 13 | } 14 | 15 | configure(loadStories, module); 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | env: 5 | global: 6 | - CC_TEST_REPORTER_ID=078550efafc3dbb8faf56159f96a1f6aed1bf6633c08cea85d7ac08a45c781b9 7 | - GIT_COMMITTED_AT=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then git log -1 --pretty=format:%ct; else git log -1 --skip 1 --pretty=format:%ct; fi) 8 | before_script: 9 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 10 | - chmod +x ./cc-test-reporter 11 | script: 12 | - npm test -- --coverage 13 | - if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT; fi 14 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | // you can use this file to add your custom webpack plugins, loaders and anything you like. 2 | // This is just the basic way to add additional webpack configurations. 3 | // For more information refer the docs: https://storybook.js.org/configurations/custom-webpack-config 4 | 5 | // IMPORTANT 6 | // When you add this file, we won't add the default configurations which is similar 7 | // to "React Create App". This only has babel loader to load JavaScript. 8 | 9 | const mainWebpackConfig = require('../webpack.config'); 10 | 11 | module.exports = { 12 | plugins: [ 13 | // your custom plugins 14 | ], 15 | module: { 16 | rules: mainWebpackConfig.module.rules, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /examples/basic-example/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; // eslint-disable-line import/no-extraneous-dependencies 4 | 5 | import 'normalize.css'; 6 | 7 | import App from './app'; 8 | 9 | const rootEl = document.getElementById('app'); 10 | 11 | ReactDOM.render( 12 | 13 | 14 | , 15 | rootEl 16 | ); 17 | /* eslint-disable global-require, import/newline-after-import */ 18 | if (module.hot) { 19 | module.hot.accept('./app', () => { 20 | const NextApp = require('./app').default; 21 | ReactDOM.render( 22 | 23 | 24 | , 25 | rootEl 26 | ); 27 | }); 28 | } 29 | /* eslint-enable global-require, import/newline-after-import */ 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Wei-Wei Wu 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 | -------------------------------------------------------------------------------- /src/utils/misc.js: -------------------------------------------------------------------------------- 1 | // from: http://stackoverflow.com/a/21913575 2 | export function getComputedTranslateY(obj) { 3 | if (!window.getComputedStyle) { 4 | return 0; 5 | } 6 | const style = getComputedStyle(obj); 7 | const transform = 8 | style.transform || style.webkitTransform || style.mozTransform; 9 | let mat = transform.match(/^matrix3d\((.+)\)$/); 10 | if (mat) { 11 | return parseFloat(mat[1].split(', ')[13]); 12 | } 13 | mat = transform.match(/^matrix\((.+)\)$/); 14 | return mat ? parseFloat(mat[1].split(', ')[5]) : 0; 15 | } 16 | 17 | // some helper functions 18 | export function scrollY() { 19 | return window.pageYOffset || window.document.documentElement.scrollTop; 20 | } 21 | 22 | export function extend(a, b) { 23 | return Object.assign(a, b); 24 | } 25 | 26 | export function getViewportH() { 27 | const client = window.document.documentElement.clientHeight; 28 | const inner = window.innerHeight; 29 | 30 | if (client < inner) { 31 | return inner; 32 | } 33 | return client; 34 | } 35 | 36 | export function isValidColor(color) { 37 | if (color.charAt(0) === '#') { 38 | const hexValue = color.substring(1); 39 | return [3, 4, 6, 8].indexOf(hexValue.length) > -1 && parseInt(hexValue, 16); 40 | } 41 | return /^(rgb|hsl)a?\((\d+%?(deg|rad|grad|turn)?[,\s]+){2,3}[\s]*[\d]+%?\)$/i.test( 42 | color 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /examples/storybooks/generic.scss: -------------------------------------------------------------------------------- 1 | .sourceLink, 2 | .sandboxButton { 3 | position: fixed; 4 | top: 0; 5 | right: 0; 6 | padding: 130px 50px 5px 50px; 7 | font: 10px helvetica, sans-serif; 8 | display: inline-block; 9 | background: rgb(12, 35, 194); 10 | color: #fff; 11 | text-decoration: none; 12 | transform: translate(50%, -50%) rotateZ(45deg); 13 | transition: background 100ms; 14 | 15 | &:hover:not(:active) { 16 | background: rgb(102, 135, 244); 17 | } 18 | } 19 | 20 | .sandboxButton { 21 | top: 30px; 22 | right: 30px; 23 | background: rgb(12, 194, 68); 24 | padding: 130px 100px 5px 100px; 25 | border: none; 26 | cursor: pointer; 27 | outline: none; 28 | &:hover:not(:active) { 29 | background: rgb(128, 242, 137); 30 | } 31 | } 32 | 33 | .component { 34 | opacity: 0; 35 | } 36 | 37 | .grid-loaded { 38 | .component { 39 | opacity: 1; 40 | } 41 | } 42 | 43 | .wrapper { 44 | &::after { 45 | content: ''; 46 | position: fixed; 47 | z-index: 1000; 48 | top: 50%; 49 | left: 50%; 50 | width: 70px; 51 | height: 70px; 52 | margin: -35px 0 0 -35px; 53 | pointer-events: none; 54 | border: 3px solid #fff; 55 | border-right-color: #323f5c; 56 | border-radius: 50%; 57 | -webkit-transition: opacity 0.3s; 58 | transition: opacity 0.3s; 59 | -webkit-animation: rotateCircle 0.7s linear infinite forwards; 60 | animation: rotateCircle 0.7s linear infinite forwards; 61 | } 62 | &.grid-loaded::after { 63 | opacity: 0; 64 | } 65 | } 66 | 67 | @-webkit-keyframes rotateCircle { 68 | to { 69 | -webkit-transform: rotate(360deg); 70 | transform: rotate(360deg); 71 | } 72 | } 73 | 74 | @keyframes rotateCircle { 75 | to { 76 | -webkit-transform: rotate(360deg); 77 | transform: rotate(360deg); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /examples/basic-example/stylesheets/app.scss: -------------------------------------------------------------------------------- 1 | @import './vendor/stylesheet'; 2 | @import './vendor/github-light'; 3 | 4 | .page-header { 5 | padding: 1rem 6rem; 6 | } 7 | 8 | .main-content { 9 | box-sizing: border-box; 10 | // background: linear-gradient(90deg, #58a8fb, #465683); 11 | background: linear-gradient(180deg, white, #58a8fb, white); 12 | } 13 | 14 | .footer { 15 | margin: 0 auto; 16 | box-sizing: border-box; 17 | padding: 2rem 6rem; 18 | max-width: 64rem; 19 | } 20 | 21 | body { 22 | background-color: #fafafa; 23 | font-family: Arial, Helvetica, sans-serif; 24 | } 25 | 26 | /** 27 | * Uncomment to simulate bootstrap environment 28 | */ 29 | // html { 30 | // box-sizing: border-box; 31 | // } 32 | 33 | // *, *::before, *::after { 34 | // box-sizing: inherit; 35 | // } 36 | 37 | .component { 38 | opacity: 0; 39 | } 40 | 41 | .grid-loaded { 42 | .component { 43 | opacity: 1; 44 | } 45 | } 46 | 47 | .wrapper { 48 | &::after { 49 | content: ''; 50 | position: fixed; 51 | z-index: 1000; 52 | top: 50%; 53 | left: 50%; 54 | width: 70px; 55 | height: 70px; 56 | margin: -35px 0 0 -35px; 57 | pointer-events: none; 58 | border: 3px solid #fff; 59 | border-right-color: #323f5c; 60 | border-radius: 50%; 61 | -webkit-transition: opacity 0.3s; 62 | transition: opacity 0.3s; 63 | -webkit-animation: rotateCircle 0.7s linear infinite forwards; 64 | animation: rotateCircle 0.7s linear infinite forwards; 65 | } 66 | &.grid-loaded::after { 67 | opacity: 0; 68 | } 69 | } 70 | 71 | @-webkit-keyframes rotateCircle { 72 | to { 73 | -webkit-transform: rotate(360deg); 74 | transform: rotate(360deg); 75 | } 76 | } 77 | 78 | @keyframes rotateCircle { 79 | to { 80 | -webkit-transform: rotate(360deg); 81 | transform: rotate(360deg); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/utils/animation-frame.js: -------------------------------------------------------------------------------- 1 | /** ******************************************* */ 2 | /** https://gist.github.com/desandro/1866474 * */ 3 | /** ******************************************* */ 4 | const prefixes = 'webkit moz ms o'.split(' '); 5 | let lastTime = 0; 6 | 7 | export function getRequestAnimationFrame() { 8 | let { requestAnimationFrame } = window; 9 | let prefix; 10 | for (let i = 0; i < prefixes.length; i += 1) { 11 | if (requestAnimationFrame) { 12 | break; 13 | } 14 | prefix = prefixes[i]; 15 | requestAnimationFrame = 16 | requestAnimationFrame || window[`${prefix}RequestAnimationFrame`]; 17 | } 18 | // fallback to setTimeout and clearTimeout if either request/cancel is not supported 19 | if (!requestAnimationFrame) { 20 | requestAnimationFrame = () => callback => { 21 | const currTime = new Date().getTime(); 22 | const timeToCall = Math.max(0, 16 - (currTime - lastTime)); 23 | const id = window.setTimeout(() => { 24 | callback(currTime + timeToCall); 25 | }, timeToCall); 26 | lastTime = currTime + timeToCall; 27 | return id; 28 | }; 29 | } 30 | return requestAnimationFrame; 31 | } 32 | 33 | export function getCancelAnimationFrame() { 34 | let { cancelAnimationFrame } = window; 35 | let prefix; 36 | for (let i = 0; i < prefixes.length; i += 1) { 37 | if (cancelAnimationFrame) { 38 | break; 39 | } 40 | prefix = prefixes[i]; 41 | cancelAnimationFrame = 42 | cancelAnimationFrame || 43 | window[`${prefix}CancelAnimationFrame`] || 44 | window[`${prefix}CancelRequestAnimationFrame`]; 45 | } 46 | // fallback to setTimeout and clearTimeout if either request/cancel is not supported 47 | if (!requestAnimationFrame || !cancelAnimationFrame) { 48 | cancelAnimationFrame = id => { 49 | window.clearTimeout(id); 50 | }; 51 | } 52 | return getCancelAnimationFrame; 53 | } 54 | -------------------------------------------------------------------------------- /examples/storybooks/basic-stack.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import dynamics from 'dynamics.js'; 3 | import IsometricGrid, { Cell } from '../../src'; 4 | 5 | const img1 = 'https://picsum.photos/100/100/?random'; 6 | const img2 = 'https://picsum.photos/200/100/?random'; 7 | const img3 = 'https://picsum.photos/300/200/?random'; 8 | const img4 = 'https://picsum.photos/400/400/?random'; 9 | const img5 = 'https://picsum.photos/500/300/?random'; 10 | const img6 = 'https://picsum.photos/700/500/?random'; 11 | const img7 = 'https://picsum.photos/800/400/?random'; 12 | 13 | class App extends Component { 14 | render() { 15 | function getRandomInt(min, max) { 16 | return Math.floor(Math.random() * (max - min + 1)) + min; 17 | } 18 | 19 | return ( 20 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ); 58 | } 59 | } 60 | 61 | export default App; 62 | -------------------------------------------------------------------------------- /examples/storybooks/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | import React from 'react'; 4 | import { storiesOf } from '@storybook/react'; 5 | // import { action } from '@storybook/addon-actions'; 6 | import { withNotes } from '@storybook/addon-notes'; 7 | import styles from './generic.scss'; 8 | import BasicStack from './basic-stack'; 9 | import BasicStackTitle from './basic-stack-with-title'; 10 | import Spread from './spread'; 11 | import SideStack from './side-stack'; 12 | import LoadedCallback from './loaded-callback'; 13 | import { handleClick, SANDBOX_URL } from './sandbox-utils'; 14 | 15 | const wrapWithSource = (node, src) => ( 16 |
17 | {node} 18 | 19 |
20 |
21 | 22 |
23 | 26 | 32 | VIEW SOURCE → 33 | 34 |
35 | ); 36 | 37 | storiesOf('Basics', module) 38 | .add( 39 | 'Basic Stack', 40 | withNotes('A basic 3d vertical stack animation')(() => 41 | wrapWithSource(, 'basic-stack.js') 42 | ) 43 | ) 44 | .add( 45 | 'Basic Stack with Title', 46 | withNotes('A basic 3d vertical stack animation and title prop')(() => 47 | wrapWithSource(, 'basic-stack-with-title.js') 48 | ) 49 | ) 50 | .add( 51 | 'Spread', 52 | withNotes( 53 | 'A spread animation to showcase a bunch of photos that are stacked togther. Using layerStyles' 54 | )(() => wrapWithSource(, 'spread.js')) 55 | ) 56 | .add( 57 | 'Side Stack', 58 | withNotes( 59 | 'Using the transform property of react-isometric-grid to display a bookshelf like grid that opens sideways. Using transform, perspective and animationOptions' 60 | )(() => wrapWithSource(, 'side-stack.js')) 61 | ); 62 | 63 | storiesOf('Advanced', module).add( 64 | 'onGridLoaded Callback', 65 | withNotes( 66 | 'Using the onGridLoaded callback to wait to display the grid until all images are loaded' 67 | )(() => wrapWithSource(, 'loaded-callback.js')) 68 | ); 69 | -------------------------------------------------------------------------------- /examples/storybooks/side-stack.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import dynamics from 'dynamics.js'; 3 | import IsometricGrid, { Cell } from '../../src'; 4 | 5 | const img1 = 'https://picsum.photos/100/100/?random'; 6 | const img2 = 'https://picsum.photos/200/100/?random'; 7 | const img3 = 'https://picsum.photos/300/200/?random'; 8 | const img4 = 'https://picsum.photos/400/400/?random'; 9 | const img5 = 'https://picsum.photos/500/300/?random'; 10 | const img6 = 'https://picsum.photos/700/500/?random'; 11 | const img7 = 'https://picsum.photos/800/400/?random'; 12 | 13 | class App extends Component { 14 | render() { 15 | const layerStyle = { 16 | transformOrigin: '50% 100%', 17 | }; 18 | 19 | return ( 20 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 68 | 72 | 76 | 77 | ); 78 | } 79 | } 80 | 81 | export default App; 82 | -------------------------------------------------------------------------------- /src/react-isometric-grid.scss: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | -webkit-box-sizing: border-box; 5 | box-sizing: border-box; 6 | } 7 | 8 | .grid, 9 | .grid__item, 10 | .grid__link { 11 | -webkit-transform-style: preserve-3d; 12 | transform-style: preserve-3d; 13 | } 14 | 15 | .grid { 16 | position: relative; 17 | margin: 0 auto; 18 | padding: 0; 19 | list-style: none; 20 | } 21 | 22 | .grid__item { 23 | -webkit-backface-visibility: hidden; 24 | backface-visibility: hidden; 25 | } 26 | 27 | .grid__link { 28 | position: relative; 29 | z-index: 1; 30 | display: block; 31 | } 32 | 33 | .grid__link.grid__link--onclick:hover { 34 | cursor: pointer; 35 | } 36 | 37 | .grid__img { 38 | display: block; 39 | max-width: 100%; 40 | } 41 | 42 | .grid__title { 43 | font-size: 0.65em; 44 | font-weight: 600; 45 | position: absolute; 46 | z-index: -1; 47 | bottom: 0; 48 | width: 100%; 49 | text-align: center; 50 | letter-spacing: 2px; 51 | text-transform: uppercase; 52 | opacity: 0; 53 | color: #fff; 54 | -webkit-transform: translate3d(0, -20px, 0); 55 | transform: translate3d(0, -20px, 0); 56 | -webkit-transition: -webkit-transform 0.3s, opacity 0.3s; 57 | transition: transform 0.3s, opacity 0.3s; 58 | } 59 | 60 | .grid__item:hover .grid__title { 61 | opacity: 1; 62 | -webkit-transform: translate3d(0, 0, 0); 63 | transform: translate3d(0, 0, 0); 64 | } 65 | 66 | .layer { 67 | position: relative; 68 | display: block; 69 | position: absolute; 70 | top: 0; 71 | left: 0; 72 | } 73 | 74 | /* Shadow effect */ 75 | 76 | .isolayer--shadow { 77 | .grid__link::before { 78 | content: ''; 79 | position: absolute; 80 | z-index: -1; 81 | top: 5px; 82 | right: 5px; 83 | bottom: 5px; 84 | left: 5px; 85 | opacity: 0.6; 86 | background: rgba(0, 0, 0, 0.8); 87 | box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.8); 88 | -webkit-transform: translateZ(-1px) scale(0.95); 89 | transform: translateZ(-1px) scale(0.95); 90 | -webkit-transition: transform 0.3s, opacity 0.3s, box-shadow 0.3s; 91 | transition: transform 0.3s, opacity 0.3s, box-shadow 0.3s; 92 | -webkit-backface-visibility: hidden; 93 | backface-visibility: hidden; 94 | } 95 | .grid__item:hover .grid__link::before { 96 | opacity: 0.2; 97 | box-shadow: 0 0 20px 10px rgba(0, 0, 0, 0.8); 98 | -webkit-transform: translateZ(-1px) scale(1); 99 | transform: translateZ(-1px) scale(1); 100 | } 101 | } 102 | 103 | .isolayer { 104 | .grid__item { 105 | padding: 15px; 106 | } 107 | .grid__link { 108 | div.layer { 109 | opacity: 0.4; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /examples/storybooks/sandbox-utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { getParameters } from 'codesandbox/lib/api/define'; 3 | 4 | const GIT_URL = 5 | 'https://api.github.com/repos/wuweiweiwu/react-isometric-grid/contents'; 6 | 7 | export const SANDBOX_URL = 'https://codesandbox.io/api/v1/sandboxes/define'; 8 | 9 | // full url for github api call 10 | const getURL = filename => `${GIT_URL}/examples/storybooks/${filename}`; 11 | 12 | // strip ../../src from the src 13 | const strip = code => code.replace('../../src', 'react-isometric-grid'); 14 | 15 | // modify code so we can just have one file in the sandbox. index.js 16 | const modify = code => { 17 | const addToTop = `import { render } from 'react-dom';\n`; 18 | const addToBottom = `\nrender(, document.getElementById('root'));`; 19 | return addToTop + code + addToBottom; 20 | }; 21 | 22 | // parse. Possible the atob throws an exception 23 | const parse = base64 => { 24 | let parsed; 25 | try { 26 | parsed = atob(base64); 27 | } catch (error) { 28 | console.error('Failed to parse base64 from GitHub', error); // eslint-disable-line no-console 29 | } 30 | return parsed; 31 | }; 32 | 33 | const html = `
`; 34 | 35 | // using codesandbox util 36 | // returns the payload to send to the define endpoint 37 | const getPayload = code => 38 | getParameters({ 39 | files: { 40 | 'package.json': { 41 | content: { 42 | dependencies: { 43 | react: 'latest', 44 | 'react-dom': 'latest', 45 | 'prop-types': 'latest', 46 | 'react-isometric-grid': 'latest', 47 | 'dynamics.js': 'latest', 48 | }, 49 | }, 50 | }, 51 | 'index.js': { 52 | content: code, 53 | }, 54 | 'index.html': { 55 | content: html, 56 | }, 57 | }, 58 | }); 59 | 60 | // set the form values and submit the form 61 | const sendSandboxRequest = parameters => { 62 | document.getElementById('codesandbox-parameters').value = parameters; 63 | document.getElementById('codesandbox-form').submit(); 64 | }; 65 | 66 | // what is called when the view sandbox element is clicked 67 | // get blob from github and the process it and send the POST request 68 | export const handleClick = file => event => { 69 | event.preventDefault(); 70 | const url = getURL(file); 71 | fetch(url) 72 | .then(response => response.json()) 73 | .catch(error => console.error('Error getting blob from GitHub:', error)) // eslint-disable-line no-console 74 | .then(response => { 75 | const parsed = parse(response.content); 76 | if (!parsed) { 77 | return; 78 | } 79 | const payload = getPayload(modify(strip(parsed))); 80 | sendSandboxRequest(payload); 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /src/cell.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import stylePropType from 'react-style-proptype'; 4 | import classNames from 'classnames'; 5 | import uniqid from 'uniqid'; 6 | 7 | import styles from './react-isometric-grid.scss'; 8 | import { isValidColor } from './utils/misc'; 9 | 10 | const DEFAULT_STYLE = { 11 | transformStyle: 'preserve-3d', 12 | width: '200px', 13 | height: '200px', 14 | }; 15 | 16 | const DEFAULT_LAYER_STYLE = { 17 | width: '200px', 18 | height: '200px', 19 | }; 20 | 21 | class Cell extends Component { 22 | render() { 23 | const { layers, href, title, style, layerStyle, onClick } = this.props; 24 | 25 | const layerList = layers.map(layer => { 26 | if (!layer) { 27 | return null; 28 | } 29 | if (isValidColor(layer)) { 30 | return ( 31 |
40 | ); 41 | } 42 | return ( 43 | 50 | ); 51 | }); 52 | 53 | return ( 54 |
  • 55 | 64 | {layerList.reverse()} 65 | {!!title && {title}} 66 | 67 |
  • 68 | ); 69 | } 70 | } 71 | 72 | Cell.propTypes = { 73 | // arry of images to be in the stack, or hex string for layer colors 74 | layers: PropTypes.arrayOf(PropTypes.string).isRequired, 75 | 76 | // onclick navigation link 77 | href: PropTypes.string, 78 | 79 | // onClick function 80 | onClick: PropTypes.func, 81 | 82 | // optional tital for the stack 83 | title: PropTypes.string, 84 | 85 | // styling for the Cell element 86 | style: stylePropType, 87 | 88 | // styling for the individual layers 89 | layerStyle: stylePropType, 90 | }; 91 | 92 | Cell.defaultProps = { 93 | href: null, 94 | onClick: null, 95 | title: null, 96 | style: DEFAULT_STYLE, 97 | layerStyle: DEFAULT_LAYER_STYLE, 98 | }; 99 | 100 | export default Cell; 101 | -------------------------------------------------------------------------------- /examples/storybooks/basic-stack-with-title.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import dynamics from 'dynamics.js'; 3 | import IsometricGrid, { Cell } from '../../src'; 4 | 5 | const img1 = 'https://picsum.photos/100/100/?random'; 6 | const img2 = 'https://picsum.photos/200/100/?random'; 7 | const img3 = 'https://picsum.photos/300/200/?random'; 8 | const img4 = 'https://picsum.photos/400/400/?random'; 9 | const img5 = 'https://picsum.photos/500/300/?random'; 10 | const img6 = 'https://picsum.photos/700/500/?random'; 11 | const img7 = 'https://picsum.photos/800/400/?random'; 12 | 13 | class App extends Component { 14 | render() { 15 | function getRandomInt(min, max) { 16 | return Math.floor(Math.random() * (max - min + 1)) + min; 17 | } 18 | 19 | return ( 20 | 47 | 51 | 55 | 59 | 63 | 67 | 71 | 75 | 79 | 83 | 84 | ); 85 | } 86 | } 87 | 88 | export default App; 89 | -------------------------------------------------------------------------------- /examples/storybooks/loaded-callback.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | import React, { Component } from 'react'; 3 | import dynamics from 'dynamics.js'; 4 | import bonzo from 'bonzo'; 5 | import IsometricGrid, { Cell } from '../../src'; 6 | 7 | import styles from './generic.scss'; 8 | 9 | const img1 = 'https://picsum.photos/100/100/?random'; 10 | const img2 = 'https://picsum.photos/200/100/?random'; 11 | const img3 = 'https://picsum.photos/300/200/?random'; 12 | const img4 = 'https://picsum.photos/400/400/?random'; 13 | const img5 = 'https://picsum.photos/500/300/?random'; 14 | const img6 = 'https://picsum.photos/700/500/?random'; 15 | const img7 = 'https://picsum.photos/800/400/?random'; 16 | 17 | class App extends Component { 18 | render() { 19 | function getRandomInt(min, max) { 20 | return Math.floor(Math.random() * (max - min + 1)) + min; 21 | } 22 | 23 | return ( 24 |
    25 | 26 |
    27 | 31 | bonzo( 32 | document.getElementsByClassName(styles.wrapper)[0] 33 | ).addClass(styles['grid-loaded']) 34 | } 35 | stackItemsAnimation={{ 36 | properties(pos) { 37 | return { 38 | translateZ: (pos + 1) * 30, 39 | rotateZ: getRandomInt(-4, 4), 40 | }; 41 | }, 42 | options(pos, itemstotal) { 43 | return { 44 | type: dynamics.bezier, 45 | duration: 500, 46 | points: [ 47 | { x: 0, y: 0, cp: [{ x: 0.2, y: 1 }] }, 48 | { x: 1, y: 1, cp: [{ x: 0.3, y: 1 }] }, 49 | ], 50 | delay: (itemstotal - pos - 1) * 40, 51 | }; 52 | }, 53 | }} 54 | style={{ 55 | height: '500px', 56 | width: '800px', 57 | }} 58 | > 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
    70 |
    71 | ); 72 | } 73 | } 74 | 75 | export default App; 76 | -------------------------------------------------------------------------------- /src/react-isometric-grid.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import stylePropType from 'react-style-proptype'; 4 | import dynamics from 'dynamics.js'; 5 | import classNames from 'classnames'; 6 | 7 | import styles from './react-isometric-grid.scss'; 8 | import IsometricGrid from './isometric-grid'; 9 | 10 | class ReactIsometricGrid extends Component { 11 | componentDidMount() { 12 | const { 13 | onGridLoaded, 14 | perspective, 15 | transform, 16 | stackItemsAnimation, 17 | } = this.props; 18 | 19 | this.isometricGrid = new IsometricGrid( 20 | document.querySelector(`.${styles.isolayer}`), 21 | { 22 | perspective, 23 | transform, 24 | stackItemsAnimation, 25 | onGridLoaded, 26 | } 27 | ); 28 | } 29 | 30 | render() { 31 | const { style, shadow, children } = this.props; 32 | 33 | return ( 34 |
    41 |
      {children}
    42 |
    43 | ); 44 | } 45 | } 46 | 47 | ReactIsometricGrid.propTypes = { 48 | // have a shadow under the cells 49 | shadow: PropTypes.bool, 50 | 51 | // ongridloaded callback 52 | onGridLoaded: PropTypes.func, 53 | 54 | // style 55 | style: stylePropType, 56 | 57 | // children (Cell elements) 58 | children: PropTypes.arrayOf(PropTypes.element).isRequired, 59 | 60 | // perspective value, # of px distance from z origin 61 | perspective: PropTypes.number, 62 | 63 | // transform of the isometric grid in 3d space 64 | // https://www.w3schools.com/cssref/css3_pr_transform.asp 65 | transform: PropTypes.string, 66 | 67 | // animation values for each cell dynamicjs 68 | stackItemsAnimation: PropTypes.shape({ 69 | // object of the properties/values you want to animate 70 | // https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function 71 | properties: PropTypes.func, 72 | 73 | // object representing the animation like duration and easing 74 | // https://github.com/michaelvillar/dynamics.js#dynamicsanimateel-properties-options 75 | options: PropTypes.func, 76 | }), 77 | }; 78 | 79 | ReactIsometricGrid.defaultProps = { 80 | shadow: false, 81 | onGridLoaded: () => {}, 82 | style: { 83 | height: '600px', 84 | width: '600px', 85 | position: 'absolute', 86 | left: 0, 87 | top: 0, 88 | }, 89 | perspective: 3000, 90 | transform: 'scale3d(0.8,0.8,1) rotateY(45deg) rotateZ(-10deg)', 91 | stackItemsAnimation: { 92 | properties(pos) { 93 | return { 94 | rotateX: (pos + 1) * -15, 95 | }; 96 | }, 97 | options(pos, totalItems) { 98 | return { 99 | type: dynamics.spring, 100 | delay: (totalItems - pos - 1) * 30, 101 | }; 102 | }, 103 | }, 104 | }; 105 | 106 | export default ReactIsometricGrid; 107 | -------------------------------------------------------------------------------- /examples/storybooks/spread.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import dynamics from 'dynamics.js'; 3 | import IsometricGrid, { Cell } from '../../src'; 4 | 5 | const img1 = 'https://picsum.photos/100/100/?random'; 6 | const img2 = 'https://picsum.photos/200/100/?random'; 7 | const img3 = 'https://picsum.photos/300/200/?random'; 8 | const img4 = 'https://picsum.photos/400/400/?random'; 9 | const img5 = 'https://picsum.photos/500/300/?random'; 10 | const img6 = 'https://picsum.photos/700/500/?random'; 11 | const img7 = 'https://picsum.photos/800/400/?random'; 12 | 13 | class App extends Component { 14 | render() { 15 | function getRandomInt(min, max) { 16 | return Math.floor(Math.random() * (max - min + 1)) + min; 17 | } 18 | 19 | const cellStyle = { 20 | border: '20px solid #fff', 21 | borderWidth: '40px 20px', 22 | boxShadow: '-1px 1px 5px rgba(0, 0, 0, 0.1)', 23 | }; 24 | 25 | const style = { 26 | transformStyle: 'flat', 27 | padding: '20px', 28 | }; 29 | 30 | return ( 31 | 58 | 63 | 68 | 73 | 78 | 83 | 88 | 93 | 98 | 99 | ); 100 | } 101 | } 102 | 103 | export default App; 104 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@weiweiwu.me. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /examples/basic-example/stylesheets/vendor/github-light.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 GitHub Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | */ 17 | 18 | .pl-c /* comment */ { 19 | color: #969896; 20 | } 21 | 22 | .pl-c1 /* constant, markup.raw, meta.diff.header, meta.module-reference, meta.property-name, support, support.constant, support.variable, variable.other.constant */, 23 | .pl-s .pl-v /* string variable */ { 24 | color: #0086b3; 25 | } 26 | 27 | .pl-e /* entity */, 28 | .pl-en /* entity.name */ { 29 | color: #795da3; 30 | } 31 | 32 | .pl-s .pl-s1 /* string source */, 33 | .pl-smi /* storage.modifier.import, storage.modifier.package, storage.type.java, variable.other, variable.parameter.function */ { 34 | color: #333; 35 | } 36 | 37 | .pl-ent /* entity.name.tag */ { 38 | color: #63a35c; 39 | } 40 | 41 | .pl-k /* keyword, storage, storage.type */ { 42 | color: #a71d5d; 43 | } 44 | 45 | .pl-pds /* punctuation.definition.string, string.regexp.character-class */, 46 | .pl-s /* string */, 47 | .pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */, 48 | .pl-sr /* string.regexp */, 49 | .pl-sr .pl-cce /* string.regexp constant.character.escape */, 50 | .pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */, 51 | .pl-sr .pl-sre /* string.regexp source.ruby.embedded */ { 52 | color: #183691; 53 | } 54 | 55 | .pl-v /* variable */ { 56 | color: #ed6a43; 57 | } 58 | 59 | .pl-id /* invalid.deprecated */ { 60 | color: #b52a1d; 61 | } 62 | 63 | .pl-ii /* invalid.illegal */ { 64 | background-color: #b52a1d; 65 | color: #f8f8f8; 66 | } 67 | 68 | .pl-sr .pl-cce /* string.regexp constant.character.escape */ { 69 | color: #63a35c; 70 | font-weight: bold; 71 | } 72 | 73 | .pl-ml /* markup.list */ { 74 | color: #693a17; 75 | } 76 | 77 | .pl-mh /* markup.heading */, 78 | .pl-mh .pl-en /* markup.heading entity.name */, 79 | .pl-ms /* meta.separator */ { 80 | color: #1d3e81; 81 | font-weight: bold; 82 | } 83 | 84 | .pl-mq /* markup.quote */ { 85 | color: #008080; 86 | } 87 | 88 | .pl-mi /* markup.italic */ { 89 | color: #333; 90 | font-style: italic; 91 | } 92 | 93 | .pl-mb /* markup.bold */ { 94 | color: #333; 95 | font-weight: bold; 96 | } 97 | 98 | .pl-md /* markup.deleted, meta.diff.header.from-file */ { 99 | background-color: #ffecec; 100 | color: #bd2c00; 101 | } 102 | 103 | .pl-mi1 /* markup.inserted, meta.diff.header.to-file */ { 104 | background-color: #eaffea; 105 | color: #55a532; 106 | } 107 | 108 | .pl-mdr /* meta.diff.range */ { 109 | color: #795da3; 110 | font-weight: bold; 111 | } 112 | 113 | .pl-mo /* meta.output */ { 114 | color: #1d3e81; 115 | } 116 | 117 | -------------------------------------------------------------------------------- /examples/basic-example/stylesheets/vendor/stylesheet.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | 6 | .btn { 7 | display: inline-block; 8 | margin-bottom: 1rem; 9 | color: rgba(255, 255, 255, 0.7); 10 | background-color: rgba(255, 255, 255, 0.08); 11 | border-color: rgba(255, 255, 255, 0.2); 12 | border-style: solid; 13 | border-width: 1px; 14 | border-radius: 0.3rem; 15 | transition: color 0.2s, background-color 0.2s, border-color 0.2s; 16 | } 17 | .btn + .btn { 18 | margin-left: 1rem; 19 | } 20 | 21 | .btn:hover { 22 | color: rgba(255, 255, 255, 0.8); 23 | text-decoration: none; 24 | background-color: rgba(255, 255, 255, 0.2); 25 | border-color: rgba(255, 255, 255, 0.3); 26 | } 27 | 28 | @media screen and (min-width: 64em) { 29 | .btn { 30 | padding: 0.75rem 1rem; 31 | } 32 | } 33 | 34 | @media screen and (min-width: 42em) and (max-width: 64em) { 35 | .btn { 36 | padding: 0.6rem 0.9rem; 37 | font-size: 0.9rem; 38 | } 39 | } 40 | 41 | @media screen and (max-width: 42em) { 42 | .btn { 43 | display: block; 44 | width: 100%; 45 | padding: 0.75rem; 46 | font-size: 0.9rem; 47 | } 48 | .btn + .btn { 49 | margin-top: 1rem; 50 | margin-left: 0; 51 | } 52 | } 53 | 54 | .page-header { 55 | color: #fff; 56 | text-align: center; 57 | background-color: #159957; 58 | background-image: linear-gradient(120deg, #155799, #159957); 59 | } 60 | 61 | @media screen and (min-width: 64em) { 62 | .page-header { 63 | padding: 5rem 6rem; 64 | } 65 | } 66 | 67 | @media screen and (min-width: 42em) and (max-width: 64em) { 68 | .page-header { 69 | padding: 3rem 4rem; 70 | } 71 | } 72 | 73 | @media screen and (max-width: 42em) { 74 | .page-header { 75 | padding: 2rem 1rem; 76 | } 77 | } 78 | 79 | .project-name { 80 | margin-top: 0; 81 | margin-bottom: 0.1rem; 82 | } 83 | 84 | @media screen and (min-width: 64em) { 85 | .project-name { 86 | font-size: 3.25rem; 87 | } 88 | } 89 | 90 | @media screen and (min-width: 42em) and (max-width: 64em) { 91 | .project-name { 92 | font-size: 2.25rem; 93 | } 94 | } 95 | 96 | @media screen and (max-width: 42em) { 97 | .project-name { 98 | font-size: 1.75rem; 99 | } 100 | } 101 | 102 | .project-tagline { 103 | margin-bottom: 2rem; 104 | font-weight: normal; 105 | opacity: 0.7; 106 | } 107 | 108 | @media screen and (min-width: 64em) { 109 | .project-tagline { 110 | font-size: 1.25rem; 111 | } 112 | } 113 | 114 | @media screen and (min-width: 42em) and (max-width: 64em) { 115 | .project-tagline { 116 | font-size: 1.15rem; 117 | } 118 | } 119 | 120 | @media screen and (max-width: 42em) { 121 | .project-tagline { 122 | font-size: 1rem; 123 | } 124 | } 125 | 126 | @media screen and (min-width: 64em) { 127 | .main-content { 128 | /* max-width: 64rem; */ 129 | padding: 2rem 6rem; 130 | /* margin: 0 auto; */ 131 | font-size: 1.1rem; 132 | } 133 | } 134 | 135 | @media screen and (min-width: 42em) and (max-width: 64em) { 136 | .main-content { 137 | padding: 2rem 4rem; 138 | font-size: 1.1rem; 139 | } 140 | } 141 | 142 | @media screen and (max-width: 42em) { 143 | .main-content { 144 | padding: 2rem 1rem; 145 | font-size: 1rem; 146 | } 147 | } 148 | 149 | .site-footer { 150 | padding-top: 2rem; 151 | margin-top: 2rem; 152 | border-top: solid 1px #7f7f80; 153 | } 154 | 155 | .site-footer-owner { 156 | display: block; 157 | font-weight: bold; 158 | } 159 | 160 | .site-footer-credits { 161 | color: #819198; 162 | } 163 | 164 | @media screen and (min-width: 64em) { 165 | .site-footer { 166 | font-size: 1rem; 167 | } 168 | } 169 | 170 | @media screen and (min-width: 42em) and (max-width: 64em) { 171 | .site-footer { 172 | font-size: 1rem; 173 | } 174 | } 175 | 176 | @media screen and (max-width: 42em) { 177 | .site-footer { 178 | font-size: 0.9rem; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const autoprefixer = require('autoprefixer'); 4 | const nodeExternals = require('webpack-node-externals'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | 7 | const target = process.env.TARGET || 'umd'; 8 | 9 | const styleLoader = { 10 | loader: 'style-loader', 11 | options: { insertAt: 'top' }, 12 | }; 13 | 14 | const fileLoader = { 15 | loader: 'file-loader', 16 | options: { name: 'static/[name].[ext]' }, 17 | }; 18 | 19 | const postcssLoader = { 20 | loader: 'postcss-loader', 21 | options: { 22 | plugins: () => [ 23 | autoprefixer({ browsers: ['IE >= 9', 'last 2 versions', '> 1%'] }), 24 | ], 25 | }, 26 | }; 27 | 28 | const cssLoader = isLocal => ({ 29 | loader: 'css-loader', 30 | options: { 31 | modules: true, 32 | '-autoprefixer': true, 33 | importLoaders: true, 34 | localIdentName: isLocal ? 'rst__[local]' : null, 35 | }, 36 | }); 37 | 38 | const config = { 39 | entry: './src/index', 40 | output: { 41 | path: path.join(__dirname, 'dist'), 42 | filename: '[name].js', 43 | libraryTarget: 'umd', 44 | library: 'ReactSortableTree', 45 | }, 46 | devtool: 'source-map', 47 | plugins: [ 48 | new webpack.EnvironmentPlugin({ NODE_ENV: 'production' }), 49 | new webpack.optimize.OccurrenceOrderPlugin(), 50 | // Use uglify for dead code removal 51 | new webpack.optimize.UglifyJsPlugin({ 52 | compress: { 53 | warnings: false, 54 | }, 55 | mangle: false, 56 | beautify: true, 57 | comments: true, 58 | }), 59 | ], 60 | module: { 61 | rules: [ 62 | { 63 | test: /\.jsx?$/, 64 | use: ['babel-loader'], 65 | exclude: path.join(__dirname, 'node_modules'), 66 | }, 67 | { 68 | test: /\.scss$/, 69 | use: [styleLoader, cssLoader(true), postcssLoader, 'sass-loader'], 70 | exclude: path.join(__dirname, 'node_modules'), 71 | }, 72 | { 73 | // Used for importing css from external modules (react-virtualized, etc.) 74 | test: /\.css$/, 75 | use: [styleLoader, cssLoader(false), postcssLoader], 76 | // use: [styleLoader, cssLoader(false)], 77 | }, 78 | ], 79 | }, 80 | }; 81 | 82 | switch (target) { 83 | case 'umd': 84 | // Exclude library dependencies from the bundle 85 | config.externals = [ 86 | nodeExternals({ 87 | // load non-javascript files with extensions, presumably via loaders 88 | whitelist: [/\.(?!(?:jsx?|json)$).{1,5}$/i], 89 | }), 90 | ]; 91 | break; 92 | case 'development': 93 | config.devtool = 'eval-source-map'; 94 | config.module.rules.push({ 95 | test: /\.(jpe?g|png|gif|ico|svg)$/, 96 | use: [fileLoader], 97 | exclude: path.join(__dirname, 'node_modules'), 98 | }); 99 | config.entry = ['react-hot-loader/patch', './examples/basic-example/index']; 100 | config.output = { 101 | path: path.join(__dirname, 'build'), 102 | filename: 'static/[name].js', 103 | }; 104 | config.plugins = [ 105 | new HtmlWebpackPlugin({ 106 | inject: true, 107 | template: './examples/basic-example/index.html', 108 | }), 109 | new webpack.EnvironmentPlugin({ NODE_ENV: 'development' }), 110 | new webpack.NoEmitOnErrorsPlugin(), 111 | ]; 112 | config.devServer = { 113 | contentBase: path.join(__dirname, 'build'), 114 | port: process.env.PORT || 3001, 115 | stats: 'minimal', 116 | }; 117 | 118 | break; 119 | case 'demo': 120 | config.module.rules.push({ 121 | test: /\.(jpe?g|png|gif|ico|svg)$/, 122 | use: [fileLoader], 123 | exclude: path.join(__dirname, 'node_modules'), 124 | }); 125 | config.entry = './examples/basic-example/index'; 126 | config.output = { 127 | path: path.join(__dirname, 'build'), 128 | filename: 'static/[name].js', 129 | }; 130 | config.plugins = [ 131 | new HtmlWebpackPlugin({ 132 | inject: true, 133 | template: './examples/basic-example/index.html', 134 | }), 135 | new webpack.EnvironmentPlugin({ NODE_ENV: 'production' }), 136 | new webpack.optimize.UglifyJsPlugin({ 137 | compress: { 138 | warnings: false, 139 | }, 140 | }), 141 | ]; 142 | 143 | break; 144 | default: 145 | } 146 | 147 | module.exports = config; 148 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-isometric-grid", 3 | "version": "0.0.3", 4 | "description": "Isometric grid component for React", 5 | "scripts": { 6 | "build": "npm run clean && cross-env NODE_ENV=production TARGET=umd webpack --bail", 7 | "build:demo": "npm run clean:demo && cross-env NODE_ENV=production TARGET=demo webpack --bail && npm run build-storybook", 8 | "clean": "rimraf dist", 9 | "clean:demo": "rimraf build", 10 | "start": "cross-env NODE_ENV=development TARGET=development webpack-dev-server --inline --hot", 11 | "lint": "eslint src examples", 12 | "prettier": "prettier --single-quote --trailing-comma es5 --write \"{src,examples}/**/*.{js,scss}\"", 13 | "prepublishOnly": "npm run lint && npm run test && npm run build", 14 | "test": "jest", 15 | "test:watch": "jest --watchAll", 16 | "test:coverage": "jest --coverage && ./cc-test-reporter after-build --id=\"078550efafc3dbb8faf56159f96a1f6aed1bf6633c08cea85d7ac08a45c781b9\"", 17 | "deploy": "npm run build:demo && gh-pages -d build", 18 | "storybook": "cross-env TARGET=development start-storybook -p ${PORT:-3001} -h 0.0.0.0", 19 | "build-storybook": "cross-env NODE_ENV=production TARGET=demo build-storybook -o build/storybook" 20 | }, 21 | "main": "dist/main.js", 22 | "files": [ 23 | "dist" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/wuweiweiwu/react-isometric-grid" 28 | }, 29 | "homepage": "https://github.com/wuweiweiwu/react-isometric-grid", 30 | "bugs": "https://github.com/wuweiweiwu/react-isometric-grid/issues", 31 | "authors": [ 32 | "Wei-Wei Wu" 33 | ], 34 | "license": "MIT", 35 | "jest": { 36 | "setupTestFrameworkScriptFile": "./node_modules/jest-enzyme/lib/index.js", 37 | "setupFiles": [ 38 | "./test-config/shim.js", 39 | "./test-config/test-setup.js" 40 | ], 41 | "moduleFileExtensions": [ 42 | "js", 43 | "jsx", 44 | "json" 45 | ], 46 | "moduleDirectories": [ 47 | "node_modules" 48 | ], 49 | "moduleNameMapper": { 50 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", 51 | "\\.(css|scss|less)$": "identity-obj-proxy" 52 | } 53 | }, 54 | "dependencies": { 55 | "bonzo": "^2.0.0", 56 | "classnames": "^2.2.5", 57 | "dynamics.js": "^1.1.5", 58 | "imagesloaded": "^4.1.4", 59 | "masonry-layout": "^4.2.1", 60 | "normalize.css": "^7.0.0", 61 | "prop-types": "^15.6.0", 62 | "react-style-proptype": "^3.1.0", 63 | "uniqid": "^4.1.1" 64 | }, 65 | "peerDependencies": { 66 | "react": "^15.3.0 || ^16.0.0", 67 | "react-dom": "^15.3.0 || ^16.0.0" 68 | }, 69 | "devDependencies": { 70 | "@storybook/addon-actions": "^3.3.9", 71 | "@storybook/addon-notes": "^3.3.11", 72 | "@storybook/addon-options": "^3.3.9", 73 | "@storybook/addon-storyshots": "^3.3.9", 74 | "@storybook/react": "^3.3.9", 75 | "autoprefixer": "^7.2.5", 76 | "babel-cli": "^6.26.0", 77 | "babel-core": "^6.26.0", 78 | "babel-eslint": "^8.2.1", 79 | "babel-jest": "^22.0.6", 80 | "babel-loader": "^7.1.2", 81 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 82 | "babel-polyfill": "^6.26.0", 83 | "babel-preset-env": "^1.6.1", 84 | "babel-preset-react": "^6.11.1", 85 | "codesandbox": "^1.1.14", 86 | "cross-env": "^5.1.3", 87 | "css-loader": "^0.28.8", 88 | "enzyme": "^3.3.0", 89 | "enzyme-adapter-react-16": "^1.1.1", 90 | "eslint": "^4.15.0", 91 | "eslint-config-airbnb": "^16.1.0", 92 | "eslint-config-prettier": "^2.9.0", 93 | "eslint-loader": "^1.9.0", 94 | "eslint-plugin-import": "^2.8.0", 95 | "eslint-plugin-jsx-a11y": "^6.0.3", 96 | "eslint-plugin-react": "^7.5.1", 97 | "file-loader": "^1.1.6", 98 | "gh-pages": "^1.1.0", 99 | "html-webpack-plugin": "^2.30.1", 100 | "identity-obj-proxy": "^3.0.0", 101 | "jest": "^22.0.6", 102 | "jest-enzyme": "^4.0.2", 103 | "json-loader": "^0.5.4", 104 | "node-sass": "^4.7.2", 105 | "postcss-loader": "^2.0.10", 106 | "prettier": "^1.10.2", 107 | "react": "^16.2.0", 108 | "react-addons-shallow-compare": "^15.6.2", 109 | "react-dnd-test-backend": "^2.5.4", 110 | "react-dnd-touch-backend": "^0.3.20", 111 | "react-dom": "^16.2.0", 112 | "react-hot-loader": "^3.1.3", 113 | "react-sortable-tree-theme-file-explorer": "^1.1.2", 114 | "react-test-renderer": "^16.2.0", 115 | "rimraf": "^2.6.2", 116 | "sass-loader": "^6.0.6", 117 | "style-loader": "^0.19.1", 118 | "webpack": "^3.10.0", 119 | "webpack-dev-server": "^3.1.11", 120 | "webpack-node-externals": "^1.6.0" 121 | }, 122 | "keywords": [ 123 | "react", 124 | "react-component" 125 | ] 126 | } 127 | -------------------------------------------------------------------------------- /src/isometric-grid.js: -------------------------------------------------------------------------------- 1 | /* 2 | Credit to codrops for most of this code. I rewrote it as a class and 3 | used npm packages instead of .js files but most of the code is from codrops.com. 4 | */ 5 | 6 | import dynamics from 'dynamics.js'; 7 | import imagesLoaded from 'imagesloaded'; 8 | import Masonry from 'masonry-layout'; 9 | 10 | import { extend } from './utils/misc'; 11 | import { 12 | getRequestAnimationFrame, 13 | getCancelAnimationFrame, 14 | } from './utils/animation-frame'; 15 | import styles from './react-isometric-grid.scss'; 16 | 17 | const defaultOptions = { 18 | // grid perspective value 19 | perspective: 0, 20 | // grid transform 21 | transform: '', 22 | // each grid item animation (for the subitems) 23 | stackItemsAnimation: { 24 | // this follows the dynamics.js (https://github.com/michaelvillar/dynamics.js) animate fn syntax 25 | // properties (pos is the current subitem position) 26 | properties(pos) { 27 | return { 28 | translateZ: (pos + 1) * 50, 29 | }; 30 | }, 31 | // animation options (pos is the current subitem position); itemstotal is the total number of subitems 32 | options() { 33 | return { 34 | type: dynamics.bezier, 35 | duration: 500, 36 | points: [ 37 | { x: 0, y: 0, cp: [{ x: 0.2, y: 1 }] }, 38 | { x: 1, y: 1, cp: [{ x: 0.3, y: 1 }] }, 39 | ], 40 | }; 41 | }, 42 | }, 43 | // callback for loaded grid 44 | onGridLoaded() { 45 | return false; 46 | }, 47 | }; 48 | 49 | // iso grid class 50 | class IsometricGrid { 51 | constructor(el, options) { 52 | this.isolayerEl = el; 53 | 54 | this.options = extend({}, defaultOptions); 55 | extend(this.options, options); 56 | 57 | if (!this.isolayerEl) { 58 | return; 59 | } 60 | 61 | this.gridEl = this.isolayerEl.querySelector(`.${styles.grid}`); 62 | 63 | // grid items 64 | this.gridItems = [].slice.call( 65 | this.gridEl.querySelectorAll(`.${styles.grid__item}`) 66 | ); 67 | this.gridItemsTotal = this.gridItems.length; 68 | 69 | this.didscroll = false; 70 | 71 | this.init(); 72 | 73 | // animation frame functions 74 | this.requestAnimationFrame = getRequestAnimationFrame(); 75 | this.cancelAnimationFrame = getCancelAnimationFrame(); 76 | } 77 | 78 | init() { 79 | const self = this; 80 | 81 | imagesLoaded(this.gridEl, () => { 82 | // initialize masonry 83 | self.msnry = new Masonry(self.gridEl, { 84 | itemSelector: `.${styles.grid__item}`, 85 | isFitWidth: true, 86 | horizontalOrder: true, 87 | }); 88 | 89 | self.isolayerEl.style.WebkitTransformStyle = 'preserve-3d'; 90 | self.isolayerEl.style.transformStyle = 'preserve-3d'; 91 | 92 | const transformValue = 93 | self.options.perspective !== 0 94 | ? `perspective(${self.options.perspective}px) ${ 95 | self.options.transform 96 | }` 97 | : self.options.transform; 98 | self.isolayerEl.style.WebkitTransform = transformValue; 99 | self.isolayerEl.style.transform = transformValue; 100 | 101 | // init/bind events 102 | self.initEvents(); 103 | 104 | // grid is "loaded" (all images are loaded) 105 | self.options.onGridLoaded(); 106 | }); 107 | } 108 | 109 | /** 110 | * Initialize/Bind events fn. 111 | */ 112 | initEvents() { 113 | const self = this; 114 | 115 | this.gridItems.forEach(item => { 116 | item.addEventListener('mouseenter', e => self.expandSubItems(e.target)); 117 | item.addEventListener('mouseleave', e => self.collapseSubItems(e.target)); 118 | }); 119 | } 120 | 121 | expandSubItems(item) { 122 | const self = this; 123 | const itemLink = item.querySelector('a'); 124 | const subItems = [].slice.call( 125 | itemLink.querySelectorAll(`.${styles.layer}`) 126 | ); 127 | const subItemsTotal = subItems.length; 128 | 129 | itemLink.style.zIndex = this.gridItemsTotal; 130 | item.style.zIndex = this.gridItemsTotal; // eslint-disable-line no-param-reassign 131 | 132 | subItems.forEach((subitem, pos) => { 133 | dynamics.stop(subitem); 134 | dynamics.animate( 135 | subitem, 136 | self.options.stackItemsAnimation.properties(pos), 137 | self.options.stackItemsAnimation.options(pos, subItemsTotal) 138 | ); 139 | }); 140 | } 141 | 142 | // eslint-disable-next-line class-methods-use-this 143 | collapseSubItems(item) { 144 | const itemLink = item.querySelector('a'); 145 | [].slice 146 | .call(itemLink.querySelectorAll(`.${styles.layer}`)) 147 | .forEach(subitem => { 148 | dynamics.stop(subitem); 149 | dynamics.animate( 150 | subitem, 151 | { 152 | // enough to reset any transform value previously set 153 | translateZ: 0, 154 | }, 155 | { 156 | duration: 100, 157 | complete() { 158 | itemLink.style.zIndex = 1; 159 | item.style.zIndex = 1; // eslint-disable-line no-param-reassign 160 | }, 161 | } 162 | ); 163 | }); 164 | } 165 | } 166 | 167 | export default IsometricGrid; 168 | -------------------------------------------------------------------------------- /examples/basic-example/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prefer-stateless-function */ 2 | import React, { Component } from 'react'; 3 | import dynamics from 'dynamics.js'; 4 | import bonzo from 'bonzo'; 5 | 6 | import IsometricGrid, { Cell } from '../../src'; 7 | 8 | import img1 from '../shared/img/1.jpg'; 9 | import img2 from '../shared/img/2.jpg'; 10 | import img3 from '../shared/img/3.jpg'; 11 | import img4 from '../shared/img/4.jpg'; 12 | import img5 from '../shared/img/5.jpg'; 13 | import img6 from '../shared/img/6.jpg'; 14 | import img7 from '../shared/img/7.jpg'; 15 | 16 | import styles from './stylesheets/app.scss'; 17 | import '../shared/favicon/favicon.ico'; 18 | 19 | class App extends Component { 20 | render() { 21 | const projectName = 'React Isometric Grid'; 22 | const authorName = 'Wei-Wei Wu'; 23 | const authorUrl = 'https://github.com/wuweiweiwu'; 24 | const githubUrl = 'https://github.com/wuweiweiwu/react-isometric-grid'; 25 | 26 | function getRandomInt(min, max) { 27 | return Math.floor(Math.random() * (max - min + 1)) + min; 28 | } 29 | 30 | return ( 31 |
    32 |
    33 |

    {projectName}

    34 | 35 |

    36 | Isometric grid layout and animation for presenting photos and 37 | projects 38 |

    39 |
    40 |
    41 |

    Demo

    42 |
    43 |
    44 | 48 | bonzo( 49 | document.getElementsByClassName(styles.wrapper)[0] 50 | ).addClass(styles['grid-loaded']) 51 | } 52 | stackItemsAnimation={{ 53 | properties(pos) { 54 | return { 55 | translateZ: (pos + 1) * 30, 56 | rotateZ: getRandomInt(-4, 4), 57 | }; 58 | }, 59 | options(pos, itemstotal) { 60 | return { 61 | type: dynamics.bezier, 62 | duration: 500, 63 | points: [ 64 | { x: 0, y: 0, cp: [{ x: 0.2, y: 1 }] }, 65 | { x: 1, y: 1, cp: [{ x: 0.3, y: 1 }] }, 66 | ], 67 | delay: (itemstotal - pos - 1) * 40, 68 | }; 69 | }, 70 | }} 71 | style={{ 72 | height: '500px', 73 | width: '800px', 74 | position: 'absolute', 75 | left: '25%', 76 | }} 77 | > 78 | 83 | 88 | 93 | 98 | 103 | 108 | 113 | 118 | 123 | 124 |
    125 |
    126 | Documentation on Github 127 |
    128 | More examples on Storybook 129 | 144 |
    145 | 146 | 147 | Fork me on GitHub 153 | 154 |
    155 | ); 156 | } 157 | } 158 | 159 | export default App; 160 | -------------------------------------------------------------------------------- /examples/shared/img/PageCloud_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | PageCloud_logo 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-isometric-grids [![Build Status](https://travis-ci.org/wuweiweiwu/react-isometric-grid.svg?branch=master)](https://travis-ci.org/wuweiweiwu/react-isometric-grid) [![npm version](https://badge.fury.io/js/react-isometric-grid.svg)](https://badge.fury.io/js/react-isometric-grid) 2 | 3 | React Isometric Grids :stuck_out_tongue: Inspired by https://github.com/codrops/IsometricGrids 4 | 5 | Featured in Codrop [Collective 386](https://tympanus.net/codrops/collective/collective-386/)! 6 | 7 | [![NPM](https://nodei.co/npm/react-isometric-grid.png)](https://nodei.co/npm/react-isometric-grid/) 8 | 9 | ## [Demo](http://weiweiwu.me/react-isometric-grid) 10 | 11 | ![Example](https://media.giphy.com/media/l4pTkeY0fDzNwhMzK/giphy.gif) 12 | 13 | ## Notes 14 | 15 | use with `normalize.css` for best results across browsers. 16 | 17 | ```bash 18 | npm i -S normalize.css 19 | 20 | # and in index.js add 21 | 22 | import 'normalize.css'; 23 | ``` 24 | 25 | ## Usage 26 | 27 | ```javascript 28 | import React, { Component } from 'react'; 29 | import IsometricGrid, { Cell } from 'react-isometric-grid'; 30 | import dynamics from 'dynamics.js'; 31 | 32 | class App extends Component { 33 | render() { 34 | function getRandomInt(min, max) { 35 | return Math.floor(Math.random() * (max - min + 1)) + min; 36 | } 37 | 38 | return ( 39 | 63 | 71 | 79 | 87 | 95 | 96 | ); 97 | } 98 | } 99 | 100 | export default App; 101 | ``` 102 | 103 | ## Options 104 | 105 | ### Isometric Grid 106 | 107 | | Prop | Type | Description | Default | 108 | | --------------------------------------------------------------------------- | -------------- | --------------------------------------------------- | ---------------------------------------------------------------------------- | 109 | | children _(required)_ | React elements | Cells inside the grid | | 110 | | [perspective](https://developer.mozilla.org/en-US/docs/Web/CSS/perspective) | number | px from the z axis | `3000` | 111 | | [transform](https://www.w3schools.com/cssref/css3_pr_transform.asp) | string | css transform applied to the whole grid | `"scale3d(0.8,0.8,1) rotateY(45deg) rotateZ(-10deg)"` | 112 | | stackItemsAnimation | object | animation properties for each cell using dynamic.js | below | 113 | | shadow | boolean | Display a shadow under the cells | false | 114 | | onGridLoaded | function | Callback when the grid is loaded | `()=>{}` | 115 | | style | object | inline css styling for the inner div | `{ height: '600px', width: '600px', position: 'absolute', left: 0, top: 0 }` | 116 | 117 | #### stackItemsAnimation prop example 118 | 119 | dynamic.js animations parameters 120 | 121 | ```javascript 122 | { 123 | // object of the properties/values you want to animate 124 | // https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function 125 | properties(pos) { 126 | return { 127 | rotateX: (pos + 1) * -15, 128 | }; 129 | }, 130 | // object representing the animation like duration and easing 131 | // https://github.com/michaelvillar/dynamics.js#dynamicsanimateel-properties-options 132 | options(pos, totalItems) { 133 | return { 134 | type: dynamics.spring, 135 | delay: (totalItems - pos - 1) * 30, 136 | }; 137 | } 138 | ``` 139 | 140 | ### Cell 141 | 142 | | Prop | Type | Description | Default | 143 | | ------------------- | --------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | 144 | | layers _(required)_ | array of string | what each layer is in the cell.
    Can be image urls or valid css colors | | 145 | | href | string | url that the image will link to when clicked | `null` | 146 | | onClick | function | what is executed when the image is clicked. If using with `href`, be sure to `preventDefault()` | `null` | 147 | | title | string | title that is under the layers. Shown on mouse over | `null` | 148 | | style | object | inline styling for the Cell component | `{ width: '200px', height: '200px', transformStyle: 'preserve-3d' }` | 149 | | layerStyle | object | inline styling for each inner layer | `{ width: '200px', height: '200px' }` | 150 | ## Compatibility 151 | 152 | It is compatible in browsers where `transform-style: 3d` is supported. 153 | 154 | ![chart](https://image.ibb.co/gH1R16/Screen_Shot_2018_01_29_at_10_07_54_PM.png) 155 | 156 | ## Troubleshooting 157 | 158 | **z-animations aren't working** Make sure you dont have `overflow` css property set. That messes up z-axis animations. https://stackoverflow.com/questions/21248111/overflow-behavior-after-using-css3-transform 159 | 160 | **2d animations are acting weird** in the `style` prop of cell. set `transformStyle: flat` SEE [#9](https://github.com/wuweiweiwu/react-isometric-grid/issues/9) https://www.w3schools.com/cssref/css3_pr_transform-style.asp 161 | 162 | **The axis of rotation is weird or not what you want** set the `transformOrigin` property of `layerStyle` prop of Cell. https://developer.mozilla.org/en-US/docs/Web/CSS/transform-origin 163 | 164 | ## Contributing 165 | 166 | After cloning the repository and running `npm install` or `yarn install` inside, you can use the following commands to develop and build the project. 167 | 168 | ```sh 169 | # Starts a webpack dev server that hosts a demo page with the component. 170 | # It uses react-hot-loader so changes are reflected on save. 171 | npm start 172 | # or 173 | yarn start 174 | 175 | # Start the storybook, which has several different examples to play with. 176 | # Also hot-reloaded. 177 | npm run storybook 178 | # or 179 | yarn storybook 180 | 181 | # Runs the library tests 182 | npm test 183 | # or 184 | yarn test 185 | 186 | # Lints the code with eslint 187 | npm run lint 188 | # or 189 | yarn lint 190 | 191 | # Lints and builds the code, placing the result in the dist directory. 192 | # This build is necessary to reflect changes if youre 193 | # `npm link`-ed to this repository from another local project. 194 | npm run build 195 | # or 196 | yarn build 197 | ``` 198 | 199 | Pull requests are welcome! 200 | 201 | ## License 202 | 203 | MIT 204 | 205 | ## Credits 206 | 207 | * [Codrops](http://www.codrops.com) 208 | * [Masonry](http://masonry.desandro.com/) by David DeSandro. 209 | * [Dynamics.js](http://dynamicsjs.com/) by Michael Villar. 210 | * [Unsplash](http://unsplash.com) 211 | * Dribbble artists: [Mike](https://dribbble.com/creativemints), [Forefathers](https://dribbble.com/forefathers), [Julian Lavallee](https://dribbble.com/JulienLavallee), [Cosmin Capitanu](https://dribbble.com/Radium) 212 | -------------------------------------------------------------------------------- /examples/storybooks/__snapshots__/storyshots.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Storyshots Advanced onGridLoaded Callback 1`] = ` 4 |
    5 | 554 |
    555 |
    560 | 565 |
    566 | 572 | 578 | VIEW SOURCE → 579 | 580 |
    581 | `; 582 | 583 | exports[`Storyshots Basics Basic Stack 1`] = ` 584 |
    585 | 1121 |
    1122 |
    1127 | 1132 |
    1133 | 1139 | 1145 | VIEW SOURCE → 1146 | 1147 |
    1148 | `; 1149 | 1150 | exports[`Storyshots Basics Basic Stack with Title 1`] = ` 1151 |
    1152 |
    1161 | 1732 |
    1733 |
    1734 |
    1739 | 1744 |
    1745 | 1751 | 1757 | VIEW SOURCE → 1758 | 1759 |
    1760 | `; 1761 | 1762 | exports[`Storyshots Basics Side Stack 1`] = ` 1763 |
    1764 | 2336 |
    2337 |
    2342 | 2347 |
    2348 | 2354 | 2360 | VIEW SOURCE → 2361 | 2362 |
    2363 | `; 2364 | 2365 | exports[`Storyshots Basics Spread 1`] = ` 2366 |
    2367 | 2973 |
    2974 |
    2979 | 2984 |
    2985 | 2991 | 2997 | VIEW SOURCE → 2998 | 2999 |
    3000 | `; 3001 | --------------------------------------------------------------------------------