├── .projectlist ├── RAWFlowStub.js.flow ├── __mocks__ ├── style.mock.js └── file.mock.js ├── dist ├── index.js.flow └── index.js ├── packages ├── react-paperjs-editor │ ├── dist │ │ ├── index.js.flow │ │ └── index.js │ ├── src │ │ ├── index.js │ │ └── components │ │ │ ├── Grid │ │ │ ├── index.js │ │ │ ├── Grid.md │ │ │ └── Grid.component.js │ │ │ ├── CircleTool │ │ │ ├── index.js │ │ │ ├── CircleTool.md │ │ │ └── CircleTool.component.js │ │ │ ├── LineTool │ │ │ ├── index.js │ │ │ ├── LineTool.md │ │ │ └── LineTool.component.js │ │ │ ├── PanAndZoom │ │ │ ├── index.js │ │ │ ├── PanAndZoom.md │ │ │ └── PanAndZoom.component.js │ │ │ ├── EllipseTool │ │ │ ├── index.js │ │ │ ├── EllipseTool.md │ │ │ └── EllipseTool.component.js │ │ │ ├── PolygonTool │ │ │ ├── index.js │ │ │ ├── PolygonTool.md │ │ │ └── PolygonTool.component.js │ │ │ ├── shared │ │ │ ├── PathTool │ │ │ │ ├── index.js │ │ │ │ └── PathTool.component.js │ │ │ └── index.js │ │ │ ├── RectangleTool │ │ │ ├── index.js │ │ │ ├── RectangleTool.md │ │ │ └── RectangleTool.component.js │ │ │ ├── SegmentPathTool │ │ │ ├── index.js │ │ │ ├── SegmentPathTool.md │ │ │ └── SegmentPathTool.component.js │ │ │ ├── FreeformPathTool │ │ │ ├── index.js │ │ │ ├── FreeformPathTool.md │ │ │ └── FreeformPathTool.component.js │ │ │ └── index.js │ ├── jest.config.js │ ├── package.json │ ├── yarn.lock │ ├── LICENSE │ └── README.md └── react-cache │ ├── index.js │ ├── README.md │ ├── package.json │ ├── LICENSE │ ├── cjs │ ├── react-cache.production.min.js │ └── react-cache.development.js │ └── umd │ ├── react-cache.production.min.js │ └── react-cache.development.js ├── .storybook ├── preview-head.html ├── preview.js ├── utils.js └── main.js ├── stories ├── Core │ ├── Setup │ │ ├── Resizable │ │ │ ├── index.js │ │ │ ├── Resizable.style.js │ │ │ └── Resizable.component.js │ │ └── index.js │ ├── shared │ │ ├── Mountable │ │ │ ├── index.js │ │ │ └── Mountable.component.js │ │ └── index.js │ ├── index.js │ └── Examples │ │ ├── Text │ │ └── index.js │ │ ├── Tool │ │ └── index.js │ │ ├── Layer │ │ └── index.js │ │ ├── index.js │ │ └── Path │ │ └── index.js ├── packages │ ├── index.js │ └── react-paperjs-editor │ │ ├── Setup │ │ ├── PanAndZoom │ │ │ ├── index.js │ │ │ ├── PanAndZoom.style.js │ │ │ └── PanAndZoom.component.js │ │ └── index.js │ │ ├── index.js │ │ ├── Tool │ │ ├── Tool.styles.js │ │ └── index.js │ │ └── shared │ │ └── index.js └── index.js ├── src ├── hoc │ ├── PaperScope │ │ ├── index.js │ │ └── PaperScope.hoc.js │ └── index.js ├── index.js ├── Paper.types.js ├── Paper.component.js ├── Paper.container.js ├── Paper.provider.js └── Paper.renderer.js ├── .eslintignore ├── .vscode ├── settings.json └── launch.json ├── lerna.json ├── test-config.js ├── .stylelintrc ├── flow-typed-update.js ├── jsconfig.json ├── .flowconfig ├── .github ├── workflows │ ├── pull_request.yml │ └── main.yml └── actions │ └── ci │ └── action.yml ├── codecov.js ├── .npmignore ├── shared └── jest.config.js ├── codecov.yml ├── rollup.config.dev.js ├── jest.config.js ├── rollup.config.prod.js ├── project-list.js ├── LICENSE ├── .eslintrc.json ├── rollup.config.common.js ├── .gitignore ├── babel.config.js ├── DEVELOPMENT.md ├── package.json └── README.md /.projectlist: -------------------------------------------------------------------------------- 1 | ** 2 | !react-cache -------------------------------------------------------------------------------- /RAWFlowStub.js.flow: -------------------------------------------------------------------------------- 1 | export default ''; 2 | -------------------------------------------------------------------------------- /__mocks__/style.mock.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /dist/index.js.flow: -------------------------------------------------------------------------------- 1 | export * from '../src'; 2 | -------------------------------------------------------------------------------- /__mocks__/file.mock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/dist/index.js.flow: -------------------------------------------------------------------------------- 1 | export * from '../src'; 2 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/index.js: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stories/Core/Setup/Resizable/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Resizable.component'; 2 | -------------------------------------------------------------------------------- /stories/Core/shared/Mountable/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Mountable.component'; 2 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/Grid/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Grid.component'; 2 | -------------------------------------------------------------------------------- /stories/packages/index.js: -------------------------------------------------------------------------------- 1 | module.exports['react-paperjs-editor'] = require('./react-paperjs-editor'); 2 | -------------------------------------------------------------------------------- /src/hoc/PaperScope/index.js: -------------------------------------------------------------------------------- 1 | export * from './PaperScope.hoc'; 2 | export { default } from './PaperScope.hoc'; 3 | -------------------------------------------------------------------------------- /src/hoc/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default as PaperScope, renderWithPaperScope } from './PaperScope'; 3 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/CircleTool/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './CircleTool.component'; 2 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/LineTool/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './LineTool.component'; 2 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/PanAndZoom/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './PanAndZoom.component'; 2 | -------------------------------------------------------------------------------- /stories/index.js: -------------------------------------------------------------------------------- 1 | module.exports.Core = require('./Core'); 2 | module.exports.packages = require('./packages'); 3 | -------------------------------------------------------------------------------- /stories/packages/react-paperjs-editor/Setup/PanAndZoom/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './PanAndZoom.component'; 2 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/EllipseTool/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './EllipseTool.component'; 2 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/PolygonTool/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './PolygonTool.component'; 2 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/shared/PathTool/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './PathTool.component'; 2 | -------------------------------------------------------------------------------- /stories/packages/react-paperjs-editor/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | './Setup/index', 3 | './Tool/index', 4 | ]; 5 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/RectangleTool/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './RectangleTool.component'; 2 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/SegmentPathTool/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './SegmentPathTool.component'; 2 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/FreeformPathTool/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './FreeformPathTool.component'; 2 | -------------------------------------------------------------------------------- /stories/packages/react-paperjs-editor/Tool/Tool.styles.js: -------------------------------------------------------------------------------- 1 | export default { 2 | container: { 3 | border: '1px solid', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | !/.storybook 2 | /coverage 3 | /packages/*/node_modules 4 | /packages/react-cache 5 | **/dist 6 | /flow-typed/npm 7 | /storybook-static -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.validate.enable": false, 3 | "flow.useNPMPackagedFlow": true, 4 | "files.eol": "\n" 5 | } -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "./", 4 | "packages/*" 5 | ], 6 | "version": "independent", 7 | "npmClient": "yarn" 8 | } 9 | -------------------------------------------------------------------------------- /stories/packages/react-paperjs-editor/shared/index.js: -------------------------------------------------------------------------------- 1 | export const ref = instanceRef => { 2 | console.log(instanceRef); // eslint-disable-line no-console 3 | }; 4 | -------------------------------------------------------------------------------- /stories/Core/shared/index.js: -------------------------------------------------------------------------------- 1 | export { default as Mountable } from './Mountable'; 2 | export const ref = instanceRef => console.log(instanceRef); // eslint-disable-line no-console 3 | -------------------------------------------------------------------------------- /test-config.js: -------------------------------------------------------------------------------- 1 | import 'raf/polyfill'; 2 | import Enzyme from 'enzyme'; 3 | import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; 4 | 5 | Enzyme.configure({ adapter: new Adapter() }); 6 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (process.env.NODE_ENV === 'development') { 4 | module.exports = require('./index.dev'); 5 | } else { 6 | module.exports = require('./index.prod'); 7 | } 8 | -------------------------------------------------------------------------------- /stories/Core/index.js: -------------------------------------------------------------------------------- 1 | module.exports.Setup = [ 2 | './index.js', 3 | ]; 4 | 5 | module.exports.Examples = [ 6 | './index.js', 7 | './Tool/index.js', 8 | './Path/index.js', 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/jest.config.js: -------------------------------------------------------------------------------- 1 | import config from 'shared/jest.config'; 2 | 3 | import pkg from './package.json'; 4 | 5 | module.exports = { 6 | ...config, 7 | displayName: pkg.name, 8 | }; 9 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "processors": ["stylelint-processor-styled-components"], 3 | "extends": [ 4 | "stylelint-config-recommended", 5 | "stylelint-config-styled-components" 6 | ], 7 | "syntax": "scss" 8 | } -------------------------------------------------------------------------------- /flow-typed-update.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import spawn from 'cross-spawn'; 3 | 4 | spawn.sync('lerna', ['exec', '--', 'flow-typed', 'update', '-s', '-i', 'peer', 'dev', '-p', path.resolve()], { stdio: 'inherit' }); 5 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (process.env.NODE_ENV === 'development') { 4 | module.exports = require('./index.dev'); 5 | } else { 6 | module.exports = require('./index.prod'); 7 | } 8 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@psychobolt/react-paperjs": ["./src/index.js"], 6 | "@psychobolt/react-paperjs-editor": ["./packages/react-paperjs-editor/src/index.js"], 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /packages/react-cache/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | module.exports = require('./cjs/react-cache.production.min.js'); 5 | } else { 6 | module.exports = require('./cjs/react-cache.development.js'); 7 | } 8 | -------------------------------------------------------------------------------- /stories/Core/Setup/Resizable/Resizable.style.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | export const container = css` 4 | border: 1px solid black; 5 | overflow: hidden; 6 | `; 7 | 8 | export const canvas = { 9 | width: '100%', 10 | height: 'calc(100vh - 26px)', 11 | }; 12 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { addDecorator } from '@storybook/react'; 2 | import { withConsole } from '@storybook/addon-console'; 3 | 4 | addDecorator((storyFn, context) => withConsole()(storyFn)(context)); 5 | 6 | export const parameters = { 7 | actions: { argTypesRegex: '^on[A-Z].*' }, 8 | }; 9 | -------------------------------------------------------------------------------- /stories/packages/react-paperjs-editor/Setup/PanAndZoom/PanAndZoom.style.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | export const container = css` 4 | &[drag-state='enabled'] { 5 | cursor: grab; 6 | } 7 | 8 | &[drag-state='dragging'] { 9 | cursor: grabbing; 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export * from './Paper.types'; 3 | export * from './hoc'; 4 | export * from './Paper.provider'; 5 | export { default as PaperContainer } from './Paper.container'; 6 | export { default as PaperRenderer } from './Paper.renderer'; 7 | export default { 8 | render: () => {}, 9 | }; 10 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [options] 2 | module.name_mapper='^\(.*\)\.\mdx?$' -> '/RAWFlowStub.js.flow' 3 | module.name_mapper='^stories/\(.*\)$' -> '/stories/\1' 4 | module.name_mapper='^@psychobolt/react-paperjs$' -> '/src/index.js' 5 | module.name_mapper='^@psychobolt/\(.*\)$' -> '/packages/\1' 6 | include_warnings=true 7 | 8 | [lints] 9 | deprecated-utility=warn 10 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | jobs: 3 | ci: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: actions/checkout@v2 7 | - uses: actions/setup-node@v2 8 | - uses: actions/cache@v2 9 | with: 10 | path: | 11 | node_modules 12 | packages/*/node_modules 13 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 14 | - uses: ./.github/actions/ci -------------------------------------------------------------------------------- /codecov.js: -------------------------------------------------------------------------------- 1 | import spawn from 'cross-spawn'; 2 | 3 | import { projectList } from './project-list'; 4 | 5 | const spawnOptions = { stdio: 'inherit' }; 6 | 7 | const scopeRegex = /^@(.+\/)+/; 8 | 9 | projectList.forEach(({ name, location }) => { 10 | spawn.sync('yarn', ['test', '--coverage', '--projects', location], spawnOptions); 11 | spawn.sync('codecov', ['--clear', `--flags=${name.replace(scopeRegex, '')}`, ...process.argv.slice(2)], spawnOptions); 12 | }); 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | __mocks__ 2 | .storybook 3 | .vscode 4 | coverage 5 | !/dist 6 | flow-typed 7 | packages 8 | shared 9 | stories 10 | storybook-static 11 | .eslintignore 12 | .eslintrc.json 13 | .flowconfig 14 | .stylelintrc 15 | babel.config.js 16 | codecov.js 17 | DEVELOPMENT.md 18 | flow-typed-update.js 19 | jest.config.js 20 | jsconfig.json 21 | lerna.json 22 | project-list.js 23 | RAWFlowStub.js.flow 24 | rollup.config.*.js 25 | test-config.js 26 | yarn.lock 27 | *.log 28 | *.yml -------------------------------------------------------------------------------- /shared/jest.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | const ROOT_RESOLVE = path.resolve(); 4 | 5 | module.exports = { 6 | setupFiles: [ 7 | `${ROOT_RESOLVE}/test-config.js`, 8 | ], 9 | collectCoverageFrom: ['src/**/*.js'], 10 | moduleNameMapper: { 11 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': `${ROOT_RESOLVE}/__mocks__/file.mock.js`, 12 | '\\.(css|less)$': `${ROOT_RESOLVE}/__mocks__/style.mock.js`, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/hoc/PaperScope/PaperScope.hoc.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const PaperScopeContext = React.createContext(); 4 | 5 | export const renderWithPaperScope = render => ( 6 | 7 | {({ paper }) => render(paper)} 8 | 9 | ); 10 | 11 | export default WrappedComponent => props => ( 12 | 13 | {context => } 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /.github/actions/ci/action.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | description: Runs linting, code coverage, and dry build 3 | runs: 4 | using: "composite" 5 | steps: 6 | - run: yarn bootstrap 7 | shell: bash 8 | - run: yarn lint 9 | shell: bash 10 | - run: yarn flow-typed-install 11 | shell: bash 12 | - run: yarn flow 13 | shell: bash 14 | - run: yarn codecov 15 | shell: bash 16 | - run: yarn build 17 | shell: bash 18 | - run: yarn build-storybook 19 | shell: bash -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | strict_yaml_branch: master 3 | 4 | coverage: 5 | status: 6 | project: 7 | default: 8 | target: auto 9 | react-paperjs: 10 | target: auto 11 | flags: 12 | - react-paperjs 13 | react-paperjs-editor: 14 | target: auto 15 | flags: 16 | - react-paperjs-editor 17 | flags: 18 | react-paperjs: 19 | paths: 20 | - src/ 21 | react-paperjs-editor: 22 | paths: 23 | - packages/react-paperjs-editor/ -------------------------------------------------------------------------------- /stories/Core/Setup/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Resizable from './Resizable'; 4 | 5 | export default { 6 | title: 'Core/Setup', 7 | }; 8 | 9 | export const DynamicContainer = () => ( 10 | <> 11 |
Drag borders to resize the canvas.
12 | 13 | 14 | ); 15 | 16 | DynamicContainer.parameters = { 17 | docs: { 18 | source: { 19 | code: require('!!raw-loader!./Resizable/Resizable.component').default, // eslint-disable-line global-require 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@psychobolt/react-paperjs-editor", 3 | "version": "0.0.16", 4 | "description": "A library of common editor components for React Paper.js", 5 | "main": "./dist/index.js", 6 | "repository": "https://github.com/psychobolt/react-paperjs/blob/master/packages/react-paperjs-editor", 7 | "author": "psychobolt", 8 | "license": "MIT", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "dependencies": { 13 | "@psychobolt/react-paperjs": "^1.0.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/react-cache/README.md: -------------------------------------------------------------------------------- 1 | # react-cache 2 | 3 | A basic cache for React applications. It also serves as a reference for more 4 | advanced caching implementations. 5 | 6 | This package is meant to be used alongside yet-to-be-released, experimental 7 | React features. It's unlikely to be useful in any other context. 8 | 9 | **Do not use in a real application.** We're publishing this early for 10 | demonstration purposes. 11 | 12 | **Use it at your own risk.** 13 | 14 | # No, Really, It Is Unstable 15 | 16 | The API ~~may~~ will change wildly between versions. 17 | -------------------------------------------------------------------------------- /packages/react-cache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "react-cache", 4 | "description": "A basic cache for React applications", 5 | "version": "2.0.0-alpha.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/facebook/react.git", 9 | "directory": "packages/react-cache" 10 | }, 11 | "files": [ 12 | "LICENSE", 13 | "README.md", 14 | "build-info.json", 15 | "index.js", 16 | "cjs/", 17 | "umd/" 18 | ], 19 | "peerDependencies": { 20 | "react": "^16.3.0-alpha.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /rollup.config.dev.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { configs } from './rollup.config.common'; 4 | 5 | function getConfig(pathname, base) { 6 | const dist = path.resolve(pathname, 'dist'); 7 | return { 8 | ...base, 9 | output: { 10 | dir: dist, 11 | entryFileNames: '[name].dev.js', 12 | chunkFileNames: '[name]-[hash].dev.js', 13 | format: 'cjs', 14 | exports: 'named', 15 | sourcemap: 'inline', 16 | }, 17 | }; 18 | } 19 | 20 | export default configs.map(([pathname, config]) => getConfig(pathname, config)); 21 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import config from 'shared/jest.config'; 4 | 5 | import { projectList } from './project-list'; 6 | import pkg from './package.json'; 7 | 8 | module.exports = { 9 | ...config, 10 | projects: projectList.reduce((paths, { location }) => { 11 | const configPath = `${location}/jest.config.js`; 12 | return fs.existsSync(configPath) ? [...paths, configPath] : paths; 13 | }, []), 14 | 15 | // root config 16 | displayName: pkg.name, 17 | testPathIgnorePatterns: [ 18 | '/node_modules/', 19 | '/packages/', 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /stories/Core/Examples/Text/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { PointText } from '@psychobolt/react-paperjs'; 4 | 5 | import { Mountable as PaperContainer, ref } from '../../shared'; 6 | 7 | export default () => ( 8 | 9 | 17 | The contents of the point text 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as FreeformPathTool } from './FreeformPathTool'; 2 | export { default as Grid } from './Grid'; 3 | export { default as LineTool } from './LineTool'; 4 | export { default as PolygonTool } from './PolygonTool'; 5 | export { default as RectangleTool } from './RectangleTool'; 6 | export { default as CircleTool } from './CircleTool'; 7 | export { default as SegmentPathTool } from './SegmentPathTool'; 8 | export { default as PanAndZoom } from './PanAndZoom'; 9 | export { default as EllipseTool } from './EllipseTool'; 10 | export { default as PathTool } from './shared/PathTool'; 11 | -------------------------------------------------------------------------------- /stories/packages/react-paperjs-editor/Setup/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import PanAndZoomReadme from 'packages/react-paperjs-editor/src/components/PanAndZoom/PanAndZoom.md'; 4 | import PanAndZoomApp from './PanAndZoom'; 5 | 6 | export default { 7 | title: 'packages/react-paperjs-editor/Setup', 8 | }; 9 | 10 | export const PanAndZoom = () => ( 11 | <> 12 |
Drag + Space bar to pan the view. Mouse wheel scroll to zoom.
13 | 14 | 15 | ); 16 | 17 | PanAndZoom.storyName = 'with Pan and Zoom'; 18 | PanAndZoom.parameters = { 19 | docs: { 20 | page: PanAndZoomReadme, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /rollup.config.prod.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { terser } from 'rollup-plugin-terser'; 3 | 4 | import { configs } from './rollup.config.common'; 5 | 6 | function getConfig(pathname, base) { 7 | const dist = path.resolve(pathname, 'dist'); 8 | return { 9 | ...base, 10 | output: { 11 | dir: dist, 12 | entryFileNames: '[name].prod.js', 13 | chunkFileNames: '[name]-[hash].prod.js', 14 | format: 'cjs', 15 | exports: 'named', 16 | }, 17 | plugins: [ 18 | ...base.plugins, 19 | terser(), 20 | ], 21 | }; 22 | } 23 | 24 | export default configs.map(([pathname, config]) => getConfig(pathname, config)); 25 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | jobs: 6 | main: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2 11 | - uses: actions/cache@v2 12 | with: 13 | path: | 14 | node_modules 15 | packages/*/node_modules 16 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 17 | - uses: ./.github/actions/ci 18 | - uses: peaceiris/actions-gh-pages@v3 19 | with: 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | publish_branch: gh-pages 22 | publish_dir: storybook-static -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Tests", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/node_modules/jest-cli/bin/jest.js", 9 | "stopOnEntry": false, 10 | "args": [ 11 | "--runInBand", 12 | "--no-cache" 13 | ], 14 | "cwd": "${workspaceRoot}", 15 | "runtimeArgs": ["--nolazy"], 16 | "console": "integratedTerminal", 17 | "sourceMaps": true, 18 | "env": { 19 | "BABEL_ENV": "test", 20 | "NODE_ENV": "development" 21 | }, 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/shared/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Paper from 'paper'; 3 | 4 | import type { MouseEventHandler } from '@psychobolt/react-paperjs'; 5 | 6 | type PathEventHandler = (path: typeof Paper.Path) => any; 7 | 8 | export type ToolDefaultProps = { 9 | onMouseDown: MouseEventHandler, 10 | onMouseDrag: MouseEventHandler, 11 | onMouseUp: MouseEventHandler, 12 | onPathAdd: PathEventHandler, 13 | onSegmentAdd: PathEventHandler, 14 | onSegmentRemove: PathEventHandler 15 | } 16 | 17 | export const toolDefaultProps = { 18 | onMouseDown: () => {}, 19 | onMouseDrag: () => {}, 20 | onMouseUp: () => {}, 21 | onPathAdd: () => {}, 22 | onSegmentAdd: () => {}, 23 | onSegmentRemove: () => {}, 24 | }; 25 | -------------------------------------------------------------------------------- /stories/Core/Examples/Tool/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CircleTool } from '@psychobolt/react-paperjs-editor'; 3 | import styled from 'styled-components'; 4 | 5 | import code from 'raw-loader!/packages/react-paperjs-editor/src/components/CircleTool/CircleTool.component'; 6 | import { Mountable } from '../../shared'; 7 | 8 | const PaperContainer = styled(Mountable)` 9 | canvas { 10 | border: 1px solid black; 11 | } 12 | `; 13 | 14 | export default { 15 | title: 'Core/Examples/Tool', 16 | }; 17 | 18 | module.exports.CircleTool = () => ( 19 | 20 | 21 | 22 | ); 23 | 24 | module.exports.CircleTool.parameters = { 25 | docs: { 26 | source: { 27 | code, 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@psychobolt/react-paperjs@^0.0.58": 6 | version "0.0.58" 7 | resolved "https://registry.yarnpkg.com/@psychobolt/react-paperjs/-/react-paperjs-0.0.58.tgz#2eec09dcc355367c1f5ee597a5612fe01491d5c0" 8 | integrity sha512-CQxYY3eCYuXRDxy8gGjfnJSqhBGDwdpG6X4bP5BVYK9hUFLW/XvsbEQPrsAupcmNplOnzN97GE2iPka/xfOrlQ== 9 | dependencies: 10 | react-is "^16.13.1" 11 | 12 | react-is@^16.13.1: 13 | version "16.13.1" 14 | resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" 15 | integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== 16 | -------------------------------------------------------------------------------- /stories/Core/Examples/Layer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { PaperContainer, Circle, Layer } from '@psychobolt/react-paperjs'; 4 | 5 | import { ref } from '../../shared'; 6 | 7 | export default () => { 8 | const Shapes = () => ( 9 | 10 | ); 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/LineTool/LineTool.md: -------------------------------------------------------------------------------- 1 | # LineTool 2 | 3 | Paper tool that allows drawing of lines on the canvas by mouse click and drag. 4 | 5 | ```jsx 6 | import { PaperContainer } from '@psychobolt/react-paperjs'; 7 | import { LineTool } from '@psychobolt/react-paperjs-editor'; 8 | 9 | import { viewProps, canvasProps } from './Scene'; 10 | import { pathProps, rest } from './path'; 11 | 12 | export () => ( 13 | 14 | 15 | 16 | ); 17 | ``` 18 | 19 | ## Props 20 | 21 | ### `pathProps: {}` 22 | 23 | Props for path. See [reference](http://paperjs.org/reference/path/). 24 | 25 | ### `...rest: {}` 26 | 27 | Props for Paper Tool. See [reference](http://paperjs.org/reference/tool/). -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/EllipseTool/EllipseTool.md: -------------------------------------------------------------------------------- 1 | # EllipseTool 2 | 3 | Paper tool that allows drawing of ellipse shapes on the canvas. 4 | 5 | ```jsx 6 | import { PaperContainer } from '@psychobolt/react-paperjs'; 7 | import { EllipseTool } from '@psychobolt/react-paperjs-editor'; 8 | 9 | import { viewProps, canvasProps } from './Scene'; 10 | import { pathProps, rest } from './path'; 11 | 12 | export () => ( 13 | 14 | 15 | 16 | ); 17 | ``` 18 | 19 | ## Props 20 | 21 | ### `pathProps: {}` 22 | 23 | Props for path. See [reference](http://paperjs.org/reference/path/). 24 | 25 | ### `...rest: {}` 26 | 27 | Props for Paper Tool. See [reference](http://paperjs.org/reference/tool/). 28 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/RectangleTool/RectangleTool.md: -------------------------------------------------------------------------------- 1 | # RectangleTool 2 | 3 | Paper tool that allows drawing of rectangle shapes on the canvas. 4 | 5 | ```jsx 6 | import { PaperContainer } from '@psychobolt/react-paperjs'; 7 | import { RectangleTool } from '@psychobolt/react-paperjs-editor'; 8 | 9 | import { viewProps, canvasProps } from './Scene'; 10 | import { pathProps, rest } from './path'; 11 | 12 | export () => ( 13 | 14 | 15 | 16 | ); 17 | ``` 18 | 19 | ## Props 20 | 21 | ### `pathProps: {}` 22 | 23 | Props for path. See [reference](http://paperjs.org/reference/path/). 24 | 25 | ### `...rest: {}` 26 | 27 | Props for Paper Tool. See [reference](http://paperjs.org/reference/tool/). -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/FreeformPathTool/FreeformPathTool.md: -------------------------------------------------------------------------------- 1 | # FreeformPathTool 2 | 3 | Paper tool that allows freeform drawing of paths on the canvas. 4 | 5 | ```jsx 6 | import { PaperContainer } from '@psychobolt/react-paperjs'; 7 | import { FreeformPathTool } from '@psychobolt/react-paperjs-editor'; 8 | 9 | import { viewProps, canvasProps } from './Scene'; 10 | import { pathProps, rest } from './path'; 11 | 12 | export () => ( 13 | 14 | 15 | 16 | ); 17 | ``` 18 | 19 | ## Props 20 | 21 | ### `pathProps: {}` 22 | 23 | Props for path. See [reference](http://paperjs.org/reference/path/). 24 | 25 | ### `...rest: {}` 26 | 27 | Props for Paper Tool. See [reference](http://paperjs.org/reference/tool/). -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/CircleTool/CircleTool.md: -------------------------------------------------------------------------------- 1 | # CircleTool 2 | 3 | Paper tool that allows dynamic drawing of circle paths on the canvas. Mouse drag to specified size. 4 | 5 | ```jsx 6 | import { PaperContainer } from '@psychobolt/react-paperjs'; 7 | import { CircleTool } from '@psychobolt/react-paperjs-editor'; 8 | 9 | import { viewProps, canvasProps } from './Scene'; 10 | import { pathProps, rest } from './path'; 11 | 12 | export () => ( 13 | 14 | 15 | 16 | ); 17 | ``` 18 | 19 | ## Props 20 | 21 | ### `pathProps: {}` 22 | 23 | Props for path. See [reference](http://paperjs.org/reference/path/). 24 | 25 | ### `...rest: {}` 26 | 27 | Props for Paper Tool. See [reference](http://paperjs.org/reference/tool/). -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/PolygonTool/PolygonTool.md: -------------------------------------------------------------------------------- 1 | # PolygonTool 2 | 3 | Paper tool that allows creating closed polygon shapes on the canvas by shift click. Close shape by clicking on a existing point. 4 | 5 | ```jsx 6 | import { PaperContainer } from '@psychobolt/react-paperjs'; 7 | import { PolygonTool } from '@psychobolt/react-paperjs-editor'; 8 | 9 | import { viewProps, canvasProps } from './Scene'; 10 | import { pathProps, rest } from './path'; 11 | 12 | export () => ( 13 | 14 | 15 | 16 | ); 17 | ``` 18 | 19 | ## Props 20 | 21 | ### `pathProps: {}` 22 | 23 | Props for path. See [reference](http://paperjs.org/reference/path/). 24 | 25 | ### `...rest: {}` 26 | 27 | Props for Paper Tool. See [reference](http://paperjs.org/reference/tool/). -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/SegmentPathTool/SegmentPathTool.md: -------------------------------------------------------------------------------- 1 | # SegmentPathTool 2 | 3 | Paper tool that allows drawing connected segmented path on the canvas. Shift click and then release shift key to complete path. 4 | 5 | ```jsx 6 | import { PaperContainer } from '@psychobolt/react-paperjs'; 7 | import { SegmentPathTool } from '@psychobolt/react-paperjs-editor'; 8 | 9 | import { viewProps, canvasProps } from './Scene'; 10 | import { pathProps, rest } from './path'; 11 | 12 | export () => ( 13 | 14 | 15 | 16 | ); 17 | ``` 18 | 19 | ## Props 20 | 21 | ### `pathProps: {}` 22 | 23 | Props for path. See [reference](http://paperjs.org/reference/path/). 24 | 25 | ### `...rest: {}` 26 | 27 | Props for Paper Tool. See [reference](http://paperjs.org/reference/tool/). -------------------------------------------------------------------------------- /stories/Core/Examples/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { isValidElementType } from 'react-is'; 3 | 4 | const reqExample = require.context('./', true, /^\.\/[\w-]+\/index\.js$/); 5 | const reqSource = require.context('!!raw-loader!./', true, /^\.\/[\w-]+\/index\.js$/); 6 | const reqReadme = require.context('./', true, /^\.\/[\w-]+\/README\.mdx?$/); 7 | 8 | export default { 9 | title: 'Core/Examples', 10 | }; 11 | 12 | reqExample.keys().forEach(folder => { 13 | const name = folder.match(/^\.\/([\w-]+)\/index\.js$/)[1]; 14 | const { default: Component } = reqExample(folder); 15 | if (isValidElementType(Component)) { 16 | const readmePath = reqReadme.keys().find(path => path.indexOf(name) > -1); 17 | const sourcePath = reqSource.keys().find(path => path.indexOf(name) > -1); 18 | module.exports[name] = () => ; 19 | module.exports[name].parameters = { 20 | docs: { 21 | ...(readmePath ? { page: reqReadme(readmePath).default } : undefined), 22 | ...(sourcePath ? { source: { code: reqSource(sourcePath).default } } : undefined), 23 | }, 24 | }; 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /project-list.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { isMatch } = require('micromatch'); 4 | const { getPackagesSync } = require('@lerna/project'); 5 | 6 | const ROOT_RESOLVE = path.resolve(); 7 | 8 | const PACKAGES = (process.env.PACKAGES || `${fs.readFileSync('.projectlist', 'utf8')}`).trim(); 9 | 10 | const EXCLUDES = []; 11 | const INCLUDES = []; 12 | 13 | if (PACKAGES) { 14 | Array.prototype.push.apply(INCLUDES, PACKAGES.split(/\s*(?:,|\n|\s)+\s*/).filter(pattern => { 15 | if (pattern.startsWith('!')) { 16 | EXCLUDES.push(pattern.substring(1)); 17 | return false; 18 | } 19 | return true; 20 | })); 21 | } 22 | 23 | const match = (strings, pattern) => strings.some(string => isMatch(string, pattern)); 24 | 25 | module.exports = { 26 | EXCLUDES, 27 | INCLUDES, 28 | projectList: getPackagesSync().filter(pkg => { 29 | const { name, location } = pkg; 30 | const strings = [name, location.replace(`${ROOT_RESOLVE}/`, '')]; 31 | return INCLUDES.some(pattern => match(strings, pattern)) 32 | && !EXCLUDES.some(pattern => match(strings, pattern)); 33 | }), 34 | }; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 psychobolt 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 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/PanAndZoom/PanAndZoom.md: -------------------------------------------------------------------------------- 1 | # Pan And Zoom 2 | 3 | Add pan and zoom controls to Paper's view. By default, space + mouse drag to pan and mouse scroll to zoom. The component will also append attribute 'drag-state' to the canvas element. 4 | 5 | Example usage: 6 | ```jsx 7 | import { PaperContainer, PanAndZoom } from '@psychobolt/react-paperjs' 8 | 9 | import Scene, { options } from './Scene'; 10 | 11 | export default () => ( 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | ``` 19 | 20 | ## Props 21 | 22 | ### `onPanEnabled?: () => any` 23 | 24 | Callback when pan is enabled. 25 | 26 | ### `onPanDisabled?: () => any` 27 | 28 | Callback when pan is disabled. 29 | 30 | ### `onZoom?: (level: number) => any` 31 | 32 | Callback when zoom leveling the canvas 33 | 34 | ### `zoomLevel?: number` 35 | 36 | Starting zoom level. Default is 1. See [reference](http://paperjs.org/reference/view/#zoom). 37 | 38 | ### `center?: object | array` 39 | 40 | The center of the view. See [reference](http://paperjs.org/reference/view/#center). -------------------------------------------------------------------------------- /stories/Core/Examples/Path/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { renderWithPaperScope, PaperContainer, Path, Rectangle } from '@psychobolt/react-paperjs'; 4 | 5 | export default { 6 | title: 'Core/Examples/Path', 7 | }; 8 | 9 | export const PathData = () => ( 10 | 11 | 15 | 16 | ); 17 | 18 | PathData.storyName = 'with pathData'; 19 | 20 | export const RectangleExample = () => { 21 | const [size, toggleSize] = React.useState([90, 60]); 22 | return ( 23 | <> 24 |
25 | 26 |
27 | 28 | {renderWithPaperScope(paper => ( 29 | 34 | ))} 35 | 36 | 37 | ); 38 | }; 39 | 40 | RectangleExample.storyName = 'Rectangle'; 41 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 psychobolt 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 | -------------------------------------------------------------------------------- /packages/react-cache/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 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 | -------------------------------------------------------------------------------- /.storybook/utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const glob = require('glob'); 3 | const _ = require('lodash'); 4 | const isClass = require('is-class'); 5 | const slash = require('slash'); 6 | 7 | const isTreeLike = input => input && !_.isFunction(input) && !_.isString(input) && !isClass(input); 8 | 9 | function flatten(tree, dir = __dirname) { 10 | if (_.isArray(tree)) return tree.map(pattern => flatten(pattern, path.resolve(dir, pattern))); 11 | if (isTreeLike(tree)) { 12 | return Object.entries(tree).reduce((paths, [current, patterns]) => { 13 | const directory = current === 'default' ? dir : path.resolve(dir, current); 14 | return paths.concat(isTreeLike(patterns) ? flatten(patterns, directory) : directory); 15 | }, []); 16 | } 17 | return slash(dir); 18 | } 19 | 20 | module.exports.getStories = (patterns, dir = __dirname) => patterns.reduce((result, pattern) => { 21 | let paths = []; 22 | glob.sync(path.resolve(dir, pattern)).forEach(storyPath => { 23 | const m = storyPath.match(/\..+$/) ? null : require(storyPath); // eslint-disable-line global-require,import/no-dynamic-require 24 | paths = paths.concat(flatten(m, storyPath)); 25 | }); 26 | return result.concat(paths); 27 | }, []); 28 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/shared/PathTool/PathTool.component.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import type { ToolEventHandler } from '@psychobolt/react-paperjs'; 4 | import Paper from 'paper'; 5 | 6 | type Path = typeof Paper.Path; 7 | type KeyEventHandler = typeof Paper.KeyEvent => any 8 | type PathEventHandler = Path => any 9 | type SegmentEventHandler = typeof Paper.Segment => any; 10 | 11 | type Props = { 12 | paper: typeof Paper.PaperScope, 13 | onKeyDown: KeyEventHandler, 14 | onKeyUp: KeyEventHandler, 15 | onMouseDown: ToolEventHandler, 16 | onMouseDrag: ToolEventHandler, 17 | onMouseUp: ToolEventHandler, 18 | onPathInit: PathEventHandler, 19 | onPathAdd: PathEventHandler, 20 | onSegmentAdd: SegmentEventHandler, 21 | onSegmentRemove: SegmentEventHandler 22 | }; 23 | 24 | export default class PathTool

extends React.Component

{ 25 | static defaultProps: Props = { 26 | paper: null, 27 | onKeyDown: () => {}, 28 | onKeyUp: () => {}, 29 | onMouseDown: () => {}, 30 | onMouseDrag: () => {}, 31 | onMouseUp: () => {}, 32 | onPathInit: () => {}, 33 | onPathAdd: () => {}, 34 | onSegmentAdd: () => {}, 35 | onSegmentRemove: () => {}, 36 | } 37 | 38 | path: Path 39 | } 40 | -------------------------------------------------------------------------------- /stories/Core/shared/Mountable/Mountable.component.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import * as ReactPaperJS from '@psychobolt/react-paperjs'; 4 | 5 | const { PaperContainer } = ReactPaperJS; 6 | 7 | type DefaultProps = { 8 | mount?: boolean 9 | }; 10 | 11 | type Props = { 12 | mount?: boolean, 13 | children: React.Node, 14 | className: string, 15 | }; 16 | 17 | type State = DefaultProps; 18 | 19 | export default class Mountable extends React.Component { 20 | static defaultProps: DefaultProps = { 21 | mount: true, 22 | } 23 | 24 | constructor(props: Props) { 25 | super(props); 26 | this.state = { 27 | mount: props.mount, 28 | }; 29 | } 30 | 31 | onClick: SyntheticMouseEvent<'div'> => void = () => this.setState(state => ({ mount: !state.mount })); 32 | 33 | render(): React.Node { 34 | const { className, mount, children, ...props } = this.props; 35 | const { mount: mounted } = this.state; 36 | return ( 37 |

38 |
39 | 40 |
41 | 42 | {mounted ? children : null} 43 | 44 |
45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaFeatures": { 5 | "legacyDecorators": true 6 | } 7 | }, 8 | "extends": ["airbnb", "plugin:flowtype/recommended"], 9 | "plugins": ["flowtype", "jest"], 10 | "rules": { 11 | "import/no-extraneous-dependencies": 0, 12 | "import/no-webpack-loader-syntax": 0, 13 | "no-confusing-arrow": ["error", {"allowParens": true}], 14 | "arrow-parens": ["error", "as-needed"], 15 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 16 | "react/jsx-props-no-spreading": 0, 17 | "react/static-property-placement": [1, "static public field"], 18 | "react/no-multi-comp": 0, 19 | "import/prefer-default-export": 0, 20 | "object-curly-newline": ["error", { "consistent": true }], 21 | "no-bitwise": ["error", { "int32Hint": true }], 22 | "no-mixed-operators": 0 23 | }, 24 | "env": { 25 | "jest/globals": true, 26 | "browser": true 27 | }, 28 | "settings": { 29 | "import/resolver": { 30 | "babel-module": { 31 | "root": ["./"], 32 | "cwd": "./" 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /rollup.config.common.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import alias from '@rollup/plugin-alias'; 4 | import resolve from '@rollup/plugin-node-resolve'; 5 | import commonjs from '@rollup/plugin-commonjs'; 6 | import babel from '@rollup/plugin-babel'; 7 | 8 | const { projectList, INCLUDES } = require('./project-list'); 9 | 10 | const ROOT_RESOLVE = path.resolve(); 11 | 12 | const config = { 13 | input: path.resolve(ROOT_RESOLVE, 'src', 'index.js'), 14 | plugins: [ 15 | alias({ 16 | entries: { 17 | paper: 'paper/dist/paper-core', 18 | }, 19 | }), 20 | resolve(), 21 | commonjs({ 22 | include: /node_modules/, 23 | }), 24 | babel({ 25 | exclude: /node_modules/, 26 | babelHelpers: 'bundled', 27 | }), 28 | ], 29 | external: [ 30 | ...projectList.map(({ name }) => name), 31 | 'paper/dist/paper-core', 32 | 'react', 33 | 'react-dom', 34 | 'react-is', 35 | 'styled-components', 36 | ], 37 | }; 38 | 39 | export const configs = INCLUDES.length === 0 && fs.statSync(config.input).isFile() 40 | ? [ROOT_RESOLVE, config] 41 | : Object.entries(projectList.reduce((cfg, { location }) => ({ 42 | ...cfg, 43 | [location]: { 44 | ...config, 45 | input: `${location}/src/index.js`, 46 | }, 47 | }), {})); 48 | 49 | export default config; 50 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/Grid/Grid.md: -------------------------------------------------------------------------------- 1 | # Grid 2 | 3 | Renders a grid onto the canvas. 4 | 5 | ```jsx 6 | import { PaperContainer } from '@psychobolt/react-paperjs'; 7 | import { Grid } from '@psychobolt/react-paperjs-editor'; 8 | 9 | import { viewProps, canvasProps } from './Scene'; 10 | import { gridProps } from './grid'; 11 | 12 | export () => ( 13 | 14 | 15 | 16 | ); 17 | ``` 18 | 19 | ## Props 20 | 21 | ### `top?: number` 22 | 23 | Starting top position of the grid relative to the canvas. Default is 0 pixels (top-most). 24 | 25 | ### `left?: number` 26 | 27 | Starting left position of the grid relative to the canvas. Default is 0 pixels (left-most). 28 | 29 | ### `right?: number` 30 | 31 | Ending right position of the grid relative to the canvas. Default value is left + width. 32 | 33 | ### `bottom?: number` 34 | 35 | Ending bottom position of the grid relative to the canvas. Default value is top + height. 36 | 37 | ### `width?: number` 38 | 39 | Width of the grid. 40 | 41 | ### `height?: number` 42 | 43 | Height of the grid. 44 | 45 | ### `cellSize?: number` 46 | 47 | Size of each cell in the grid. The default value is 50 pixels. 48 | 49 | ### `strokeColor?: string` 50 | 51 | Color of each grid line. 52 | 53 | ### `strokeWidth?: number` 54 | 55 | Width of each grid line. 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # dist 61 | **/dist/* 62 | !**/dist/index.js 63 | !**/dist/*.js.flow 64 | 65 | # flow-typed 66 | flow-typed/npm 67 | 68 | # Storybook 69 | storybook-static 70 | 71 | # only support Yarn 72 | package-lock.json 73 | 74 | # Mac files 75 | .DS_Store 76 | 77 | # Cache files 78 | .cache 79 | 80 | # Node Version Manager 81 | .nvmrc -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const { getStories } = require('./utils'); 2 | 3 | module.exports = { 4 | stories: getStories(['../stories']), 5 | features: { 6 | postcss: false, 7 | }, 8 | addons: [ 9 | '@storybook/addon-links', 10 | '@storybook/addon-essentials', 11 | ], 12 | // See https://github.com/storybookjs/storybook/blob/master/addons/docs/src/frameworks/common/preset.ts, to configure 13 | webpackFinal: async config => ({ 14 | ...config, 15 | module: { 16 | ...config.module, 17 | rules: [ 18 | ...config.module.rules.map(rule => { 19 | if (rule.test.test('.md')) { 20 | return {}; 21 | } 22 | if (rule.exclude && rule.exclude.test('.stories.mdx')) { 23 | return { ...rule, test: /\.md$/ }; 24 | } 25 | if (rule.test.test('.stories.mdx')) { 26 | return { ...rule, test: /\.mdx$/ }; 27 | } 28 | if (rule.test.test('.stories.js')) { 29 | return { 30 | ...rule, 31 | test: /\.jsx?$/, 32 | include: /stories/, 33 | }; 34 | } 35 | return rule; 36 | }), 37 | { 38 | test: /\.jsx?$/, 39 | exclude: /node_modules/, 40 | loader: 'source-map-loader', 41 | enforce: 'pre', 42 | }, 43 | ], 44 | }, 45 | resolve: { 46 | ...config.resolve, 47 | alias: { 48 | ...(config.resolve ? config.resolve.alias : undefined), 49 | react: require.resolve('react'), 50 | 'react-dom': require.resolve('react-dom'), 51 | 'styled-components': require.resolve('styled-components'), 52 | }, 53 | }, 54 | }), 55 | }; 56 | -------------------------------------------------------------------------------- /src/Paper.types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import Paper from 'paper'; 4 | 5 | export type Types = { 6 | [type: string]: (props: {}, paper: typeof Paper.PaperScope, children?: Node) => Object 7 | }; 8 | 9 | export type Components = { 10 | [key: string]: React.AbstractComponent 11 | }; 12 | 13 | const PAPER = { 14 | Tool: 'Tool', 15 | Layer: 'Layer', 16 | Group: 'Group', 17 | Path: 'Path', 18 | Line: 'Line', 19 | Raster: 'Raster', 20 | Rectangle: 'Rectangle', 21 | Circle: 'Circle', 22 | PointText: 'PointText', 23 | }; 24 | 25 | export const CONSTANTS = { 26 | PaperScope: 'PaperScope', 27 | ...PAPER, 28 | }; 29 | 30 | const TYPES: Types = { 31 | [CONSTANTS.PaperScope]: (props, paper) => new paper.PaperScope(), 32 | [CONSTANTS.Tool]: (props, paper) => new paper.Tool(props), 33 | [CONSTANTS.Layer]: (props, paper) => new paper.Layer(props), 34 | [CONSTANTS.Group]: (props, paper) => new paper.Group(props), 35 | [CONSTANTS.Path]: (props, paper) => new paper.Path(props), 36 | [CONSTANTS.Raster]: (props, paper) => new paper.Raster(props), 37 | [CONSTANTS.Line]: (props, paper) => new paper.Path.Line(props), 38 | [CONSTANTS.Rectangle]: (props, paper) => new paper.Path.Rectangle(props), 39 | [CONSTANTS.Circle]: (props, paper) => new paper.Path.Circle(props), 40 | [CONSTANTS.PointText]: (props, paper, children) => new paper.PointText({ 41 | ...props, 42 | content: children, 43 | }), 44 | }; 45 | 46 | export default TYPES; 47 | 48 | export const components: Components = Object.entries(PAPER).reduce( 49 | (types: Components, [key, Type]) => ({ 50 | ...types, 51 | // $FlowFixMe 52 | [key]: React.forwardRef((props, ref) => ), 53 | }), 54 | {}, 55 | ); 56 | 57 | export const { 58 | Tool, 59 | Layer, 60 | Group, 61 | Path, 62 | Line, 63 | Raster, 64 | Rectangle, 65 | Circle, 66 | PointText, 67 | } = components; 68 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/Grid/Grid.component.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import * as ReactPaperJS from '@psychobolt/react-paperjs'; 4 | import Paper from 'paper'; 5 | 6 | const { Layer, Group, Line } = ReactPaperJS; 7 | 8 | type LayerType = typeof Paper.Layer; 9 | 10 | type Props = { 11 | top: number, 12 | left: number, 13 | right: number, 14 | bottom: number, 15 | width: number, 16 | height: number, 17 | cellSize: number, 18 | strokeColor: string, 19 | strokeWidth: number, 20 | innerRef: React.Ref 21 | }; 22 | 23 | const Grid = ({ width, height, top = 0, left = 0, right = left + width, bottom = top + height, cellSize = 50, strokeColor = '#D0D0D0', strokeWidth = 1, innerRef }: Props) => { 24 | const x = Math.ceil(left / cellSize) * cellSize; 25 | const y = Math.ceil(top / cellSize) * cellSize; 26 | const cols = Math.ceil((right - left) / cellSize); 27 | const rows = Math.ceil((bottom - top) / cellSize); 28 | const verticalLines = []; 29 | const horizontalLines = []; 30 | for (let i = 0; i <= cols; i += 1) { 31 | const position = x + (i * cellSize); 32 | verticalLines.push(); 39 | } 40 | for (let i = 0; i <= rows; i += 1) { 41 | const position = y + (i * cellSize); 42 | horizontalLines.push(); 49 | } 50 | return ( 51 | 52 | 53 | {verticalLines} 54 | 55 | 56 | {horizontalLines} 57 | 58 | 59 | ); 60 | }; 61 | 62 | export default (React.forwardRef( 63 | (props, ref) => , 64 | ): React.AbstractComponent); 65 | -------------------------------------------------------------------------------- /stories/packages/react-paperjs-editor/Setup/PanAndZoom/PanAndZoom.component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PaperContainer, Layer, Circle, renderWithPaperScope } from '@psychobolt/react-paperjs'; 3 | import { Grid, PanAndZoom } from '@psychobolt/react-paperjs-editor'; 4 | import styled from 'styled-components'; 5 | 6 | import * as styles from './PanAndZoom.style'; 7 | import { ref } from '../../shared'; 8 | 9 | const width = 500; 10 | const height = 500; 11 | 12 | const Container = styled(PaperContainer)` 13 | ${styles.container} 14 | `; 15 | 16 | export default () => ( 17 | paper.view.element.focus()} 24 | > 25 | { 34 | console.log('Pan enabled'); // eslint-disable-line no-console 35 | }} 36 | onPanDisabled={() => { 37 | console.log('Pan disabled'); // eslint-disable-line no-console 38 | }} 39 | onZoom={level => { 40 | console.log(`Zoom: ${level}`); // eslint-disable-line no-console 41 | }} 42 | > 43 | {renderWithPaperScope(paper => { 44 | const { top, left, right, bottom } = paper.view.bounds; 45 | return ( 46 | 56 | ); 57 | })} 58 | 59 | 65 | 66 | 67 | 68 | ); 69 | -------------------------------------------------------------------------------- /stories/Core/Setup/Resizable/Resizable.component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Resizable } from 're-resizable'; 3 | import styled from 'styled-components'; 4 | 5 | import { renderWithPaperScope, PaperContainer, Circle } from '@psychobolt/react-paperjs'; 6 | 7 | import { ref } from '../../shared'; 8 | import * as styles from './Resizable.style'; 9 | 10 | const Container = styled(Resizable)` 11 | ${styles.container} 12 | `; 13 | 14 | export default class extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | width: styles.canvas.width, 19 | height: styles.canvas.height, 20 | }; 21 | this.container = React.createRef(); 22 | } 23 | 24 | onResizeStop = (event, direction, refToElement) => { 25 | const width = refToElement.clientWidth; 26 | const height = refToElement.clientHeight; 27 | Object.assign(this.container.current.props.paper.view.viewSize, { width, height }); 28 | } 29 | 30 | render() { 31 | const { width, height } = this.state; 32 | return ( 33 | 34 | ({ 41 | onResize: () => { 42 | const { width: vWidth, height: vHeight } = paper.view.viewSize; 43 | this.setState({ width: `${vWidth}px`, height: `${vHeight}px` }); 44 | }, 45 | })} 46 | > 47 | {renderWithPaperScope(paper => { 48 | const { x, y } = paper.view.center; 49 | return ( 50 | 56 | ); 57 | })} 58 | 59 | 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Paper.component.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | function indexProps(dictionary, props) { 4 | Object.entries(props).forEach(([key, value]) => { 5 | if (key === 'children') return; 6 | const values = dictionary[key]; 7 | if (values) { 8 | values.push(value); 9 | } else { 10 | dictionary[key] = [value]; // eslint-disable-line no-param-reassign 11 | } 12 | }); 13 | } 14 | 15 | export function diffProps(oldProps, newProps) { 16 | const updatePayload = []; // schema: [propKey1, value2, propKey2, value2, ...] 17 | const propChanges = {}; // schema: { propKey1: [oldValue1, newValue1], ... } 18 | indexProps(propChanges, oldProps); 19 | indexProps(propChanges, newProps); 20 | Object.entries(propChanges).forEach(([key, values]) => { 21 | if (values.length === 1) { 22 | if (key in newProps) { 23 | const [value] = values; 24 | updatePayload.push(key, value); 25 | } else { 26 | updatePayload.push(key, null); 27 | } 28 | } else if (values.length === 2) { 29 | const [preValue, nextValue] = values; 30 | if (!_.isEqual(preValue, nextValue)) { 31 | updatePayload.push(key, nextValue); 32 | } 33 | } 34 | }); 35 | return updatePayload.length ? updatePayload : null; 36 | } 37 | 38 | /* eslint no-param-reassign: 39 | ["error", { "props": true, "ignorePropertyModificationsFor": ["instance"] }] 40 | */ 41 | export function updateProps(ref, updatePayload, type) { 42 | for (let i = 1; i < updatePayload.length; i += 2) { 43 | const key = updatePayload[i - 1]; 44 | const value = updatePayload[i]; 45 | let instance = ref; 46 | if (type === 'Rectangle') { 47 | instance = instance.bounds; 48 | } 49 | if (key === 'center') { 50 | instance.position = value; 51 | } else if (key === 'from') { 52 | instance.firstSegment.point = value; 53 | } else if (key === 'to') { 54 | instance.lastSegment.point = value; 55 | } else if (key in instance) { 56 | instance[key] = value; 57 | } else { 58 | console.log(`instance does not have property ${key}`, instance); // eslint-disable-line no-console 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/FreeformPathTool/FreeformPathTool.component.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import * as ReactPaperJS from '@psychobolt/react-paperjs'; 4 | import Paper from 'paper'; 5 | 6 | import PathTool from '../shared/PathTool'; 7 | 8 | type ToolType = typeof Paper.Tool; 9 | type ToolEvent = typeof Paper.ToolEvent; 10 | 11 | const { Tool, PaperScope } = ReactPaperJS; 12 | 13 | type Props = { 14 | pathProps: { 15 | strokeColor: string, 16 | }, 17 | innerRef: React.Ref 18 | }; 19 | 20 | const MOUSE_LEFT_CODE = 0; 21 | 22 | @PaperScope 23 | class FreeformPathTool extends PathTool { 24 | static defaultProps = { 25 | ...PathTool.defaultProps, 26 | pathProps: { 27 | strokeColor: 'black', 28 | }, 29 | } 30 | 31 | onMouseDown = (toolEvent: ToolEvent) => { 32 | const { pathProps, onMouseDown, onPathInit, paper } = this.props; 33 | if (toolEvent.event.button === MOUSE_LEFT_CODE) { 34 | const path = new paper.Path(pathProps); 35 | this.path = path; 36 | onPathInit(path); 37 | } 38 | onMouseDown(toolEvent); 39 | } 40 | 41 | onMouseDrag = (toolEvent: ToolEvent) => { 42 | const { onMouseDrag } = this.props; 43 | if (toolEvent.event.buttons === 1) { 44 | this.path.add(toolEvent.point); 45 | } 46 | onMouseDrag(toolEvent); 47 | } 48 | 49 | onMouseUp = (toolEvent: ToolEvent) => { 50 | const { path } = this; 51 | const { onMouseUp, onPathAdd } = this.props; 52 | if (path) { 53 | onPathAdd(path); 54 | this.path = null; 55 | } 56 | onMouseUp(toolEvent); 57 | } 58 | 59 | render() { 60 | const { 61 | pathProps, onMouseDown, onMouseDrag, onMouseUp, onPathAdd, paper, innerRef, ...rest 62 | } = this.props; 63 | return ( 64 | 72 | ); 73 | } 74 | } 75 | 76 | export default (React.forwardRef( 77 | (props, ref) => , 78 | ): React.AbstractComponent); 79 | -------------------------------------------------------------------------------- /packages/react-cache/cjs/react-cache.production.min.js: -------------------------------------------------------------------------------- 1 | /** @license React v16.6.1 2 | * react-cache.production.min.js 3 | * 4 | * Copyright (c) Facebook, Inc. and its affiliates. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | 'use strict';Object.defineProperty(exports,"__esModule",{value:!0});var l=require("react"),m=require("scheduler"),n=l.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher;function p(c,e){var f=n.current;if(null===f)throw Error("react-cache: read and preload may only be called from within a component's render. They are not supported in event handlers or lifecycle methods.");return f.readContext(c,e)}function q(c){return c} 11 | var r=function(c){function e(){!1===h&&k>g&&(h=!0,m.unstable_scheduleCallback(f))}function f(){h=!1;var d=g;if(null!==a)for(var b=a.previous;k>d&&null!==b;){var c=b.onDelete,e=b.previous;b.onDelete=null;b.previous=b.next=null;b===a?a=b=null:(a.previous=e,e.next=a,b=e);--k;c()}}var g=c,a=null,k=0,h=!1;return{add:function(d,b){d={value:d,onDelete:b,next:null,previous:null};null===a?d.previous=d.next=d:(b=a.previous,b.next=d,d.previous=b,a.previous=d,d.next=a);a=d;k+=1;return d},update:function(a,b){a.value= 12 | b},access:function(d){var b=d.next;if(null!==b){var c=a;if(a!==d){var f=d.previous;f.next=b;b.previous=f;b=c.previous;b.next=d;d.previous=b;c.previous=d;d.next=c;a=d}}e();return d.value},setLimit:function(a){g=a;e()}}}(500),t=new Map,u=l.createContext(null); 13 | function v(c,e,f,g){var a=t.get(c);void 0===a&&(a=new Map,t.set(c,a));var k=a.get(g);if(void 0===k){e=e(f);e.then(function(a){if(0===h.status){var b=h;b.status=1;b.value=a}},function(a){if(0===h.status){var b=h;b.status=2;b.value=a}});var h={status:0,value:e};c=r.add(h,w.bind(null,c,g));a.set(g,c);return h}return r.access(k)}function w(c,e){var f=t.get(c);void 0!==f&&(f.delete(e),0===f.size&&t.delete(c))} 14 | exports.unstable_createResource=function(c,e){var f=void 0!==e?e:q,g={read:function(a){p(u);var e=f(a);a=v(g,c,a,e);switch(a.status){case 0:throw a.value;case 1:return a.value;case 2:throw a.value;}},preload:function(a){p(u);var e=f(a);v(g,c,a,e)}};return g};exports.unstable_setGlobalCacheLimit=function(c){r.setLimit(c)}; 15 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/LineTool/LineTool.component.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import * as ReactPaperJS from '@psychobolt/react-paperjs'; 4 | import Paper from 'paper'; 5 | 6 | import PathTool from '../shared/PathTool'; 7 | 8 | const { Tool, PaperScope } = ReactPaperJS; 9 | 10 | type ToolType = typeof Paper.Tool; 11 | type ToolEvent = typeof Paper.ToolEvent; 12 | 13 | type Props = { 14 | pathProps: { 15 | strokeColor: string, 16 | }, 17 | innerRef: React.Ref 18 | }; 19 | 20 | const MOUSE_LEFT_CODE = 0; 21 | 22 | @PaperScope 23 | class LineTool extends PathTool { 24 | static defaultProps = { 25 | ...PathTool.defaultProps, 26 | pathProps: { 27 | strokeColor: 'black', 28 | }, 29 | }; 30 | 31 | onMouseDown = (toolEvent: ToolEvent) => { 32 | const { pathProps, onMouseDown, onPathInit, paper } = this.props; 33 | if (toolEvent.event.button === MOUSE_LEFT_CODE) { 34 | const path = new paper.Path(pathProps); 35 | path.add(toolEvent.point); 36 | this.path = path; 37 | onPathInit(path); 38 | } 39 | onMouseDown(toolEvent); 40 | } 41 | 42 | onMouseDrag = (toolEvent: ToolEvent) => { 43 | const { path } = this; 44 | const { onMouseDrag } = this.props; 45 | if (toolEvent.event.buttons === 1) { 46 | path.removeSegment(1); 47 | path.addSegment(toolEvent.point); 48 | path.selected = true; 49 | } 50 | onMouseDrag(toolEvent); 51 | } 52 | 53 | onMouseUp = (toolEvent: ToolEvent) => { 54 | const { path } = this; 55 | const { onMouseUp, onPathAdd } = this.props; 56 | if (path) { 57 | path.selected = false; 58 | onPathAdd(path); 59 | this.path = null; 60 | } 61 | onMouseUp(toolEvent); 62 | } 63 | 64 | render() { 65 | const { 66 | pathProps, onMouseDown, onMouseDrag, onMouseUp, onPathAdd, innerRef, ...rest 67 | } = this.props; 68 | return ( 69 | 76 | ); 77 | } 78 | } 79 | 80 | export default (React.forwardRef( 81 | (props, ref) => , 82 | ): React.AbstractComponent); 83 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const { projectList } = require('./project-list'); 2 | 3 | const aliases = process.env.BABEL_ENV === 'rollup' 4 | ? {} 5 | : projectList.reduce((projectAliases, { name, location }) => ({ 6 | ...projectAliases, 7 | [name]: `${location}/${process.env.BABEL_ENV === 'test' ? 'src' : 'dist'}`, 8 | }), {}); 9 | 10 | module.exports = { 11 | presets: [ 12 | [ 13 | '@babel/preset-env', 14 | { 15 | modules: false, 16 | }, 17 | ], 18 | '@babel/preset-react', 19 | '@babel/preset-flow', 20 | ], 21 | plugins: [ 22 | // Stage 1 23 | '@babel/plugin-proposal-export-default-from', 24 | '@babel/plugin-proposal-optional-chaining', 25 | '@babel/plugin-proposal-nullish-coalescing-operator', 26 | '@babel/plugin-proposal-do-expressions', 27 | // Stage 2 28 | [ 29 | '@babel/plugin-proposal-decorators', 30 | { 31 | legacy: true, 32 | }, 33 | ], 34 | '@babel/plugin-proposal-export-namespace-from', 35 | '@babel/plugin-proposal-numeric-separator', 36 | '@babel/plugin-proposal-throw-expressions', 37 | // Stage 3 38 | '@babel/plugin-syntax-dynamic-import', 39 | [ 40 | '@babel/plugin-proposal-class-properties', 41 | { 42 | loose: true, 43 | }, 44 | ], 45 | [ 46 | '@babel/plugin-proposal-private-methods', 47 | { 48 | loose: true, 49 | }, 50 | ], 51 | [ 52 | '@babel/plugin-proposal-private-property-in-object', 53 | { 54 | loose: true, 55 | }, 56 | ], 57 | '@babel/plugin-proposal-json-strings', 58 | // Custom 59 | [ 60 | 'lodash', 61 | { 62 | id: ['lodash'], 63 | }, 64 | ], 65 | [ 66 | 'module-resolver', 67 | { 68 | root: ['./'], 69 | cwd: './', 70 | alias: { 71 | ...aliases, 72 | 'react-cache': './packages/react-cache', // See: https://github.com/facebook/react/issues/14780#issuecomment-461861948 73 | }, 74 | }, 75 | ], 76 | 'babel-plugin-styled-components', 77 | ], 78 | env: { 79 | commonjs: { 80 | plugins: [ 81 | '@babel/plugin-transform-modules-commonjs', 82 | ], 83 | }, 84 | test: { 85 | plugins: [ 86 | '@babel/plugin-transform-modules-commonjs', 87 | 'dynamic-import-node', 88 | ], 89 | }, 90 | }, 91 | }; 92 | -------------------------------------------------------------------------------- /packages/react-cache/umd/react-cache.production.min.js: -------------------------------------------------------------------------------- 1 | /** @license React v16.6.1 2 | * react-cache.production.min.js 3 | * 4 | * Copyright (c) Facebook, Inc. and its affiliates. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 'use strict';(function(k,m){"object"===typeof exports&&"undefined"!==typeof module?m(exports,require("react"),require("scheduler")):"function"===typeof define&&define.amd?define(["exports","react","scheduler"],m):m(k.ReactCache={},k.React,k.Scheduler)})(this,function(k,m,u){function q(c,e){var f=v.current;if(null===f)throw Error("react-cache: read and preload may only be called from within a component's render. They are not supported in event handlers or lifecycle methods.");return f.readContext(c, 10 | e)}function w(c){return c}function r(c,e,f,g){var a=n.get(c);void 0===a&&(a=new Map,n.set(c,a));var l=a.get(g);if(void 0===l){e=e(f);e.then(function(a){if(0===h.status){var b=h;b.status=1;b.value=a}},function(a){if(0===h.status){var b=h;b.status=2;b.value=a}});var h={status:0,value:e};c=p.add(h,x.bind(null,c,g));a.set(g,c);return h}return p.access(l)}function x(c,e){var f=n.get(c);void 0!==f&&(f.delete(e),0===f.size&&n.delete(c))}var v=m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher, 11 | p=function(c){function e(){!1===h&&l>g&&(h=!0,u.unstable_scheduleCallback(f))}function f(){h=!1;var b=g;if(null!==a)for(var d=a.previous;l>b&&null!==d;){var c=d.onDelete,e=d.previous;d.onDelete=null;d.previous=d.next=null;d===a?a=d=null:(a.previous=e,e.next=a,d=e);--l;c()}}var g=c,a=null,l=0,h=!1;return{add:function(b,d){b={value:b,onDelete:d,next:null,previous:null};null===a?b.previous=b.next=b:(d=a.previous,d.next=b,b.previous=d,a.previous=b,b.next=a);a=b;l+=1;return b},update:function(a,d){a.value= 12 | d},access:function(b){var d=b.next;if(null!==d){var c=a;if(a!==b){var f=b.previous;f.next=d;d.previous=f;d=c.previous;d.next=b;b.previous=d;c.previous=b;b.next=c;a=b}}e();return b.value},setLimit:function(a){g=a;e()}}}(500),n=new Map,t=m.createContext(null);k.unstable_createResource=function(c,e){var f=void 0!==e?e:w,g={read:function(a){q(t);var e=f(a);a=r(g,c,a,e);switch(a.status){case 0:throw a.value;case 1:return a.value;case 2:throw a.value;}},preload:function(a){q(t);var e=f(a);r(g,c,a,e)}}; 13 | return g};k.unstable_setGlobalCacheLimit=function(c){p.setLimit(c)};Object.defineProperty(k,"__esModule",{value:!0})}); 14 | -------------------------------------------------------------------------------- /src/Paper.container.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import Paper from 'paper'; 4 | 5 | import PaperRenderer from './Paper.renderer'; 6 | import PaperProvider, { type Props as ProviderProps } from './Paper.provider'; // eslint-disable-line no-unused-vars 7 | import { CONSTANTS } from './Paper.types'; 8 | 9 | type PaperScope = typeof Paper.PaperScope; 10 | 11 | type Props = { 12 | paper: PaperScope, 13 | renderer: PaperRenderer, 14 | canvasProps: {}, 15 | viewProps: {}, 16 | onMount?: PaperScope => void, 17 | className: string, 18 | } & ProviderProps; 19 | 20 | // $FlowFixMe 21 | @PaperProvider 22 | class PaperContainer extends React.Component { 23 | static defaultProps = { 24 | onMount: () => {}, 25 | }; 26 | 27 | mountNode: any; 28 | 29 | canvas: { current: null | HTMLCanvasElement }; 30 | 31 | constructor(props: Props) { 32 | super(props); 33 | const { renderer, paper } = this.props; 34 | this.mountNode = renderer.reconciler.createContainer(paper); 35 | this.canvas = React.createRef(); 36 | } 37 | 38 | componentDidMount() { 39 | const { paper, onMount } = this.props; 40 | if (this.canvas.current) { 41 | paper.setup(this.canvas.current); 42 | const layer = this.newLayer({ name: '$$default' }); 43 | this.newLayer({ name: '$$metadata' }); 44 | layer.activate(); 45 | this.update(); 46 | } 47 | if (onMount) { 48 | onMount(paper); 49 | } 50 | } 51 | 52 | componentDidUpdate() { 53 | this.update(); 54 | } 55 | 56 | componentWillUnmount() { 57 | const { renderer } = this.props; 58 | renderer.reconciler.updateContainer(null, this.mountNode, this); 59 | } 60 | 61 | update = () => { 62 | const { paper, viewProps, renderer, children } = this.props; 63 | Object.assign(paper.view, viewProps); 64 | renderer.reconciler.updateContainer(children, this.mountNode, this); 65 | }; 66 | 67 | newLayer(options = {}) { 68 | const { paper, renderer } = this.props; 69 | return paper.project 70 | .addLayer(renderer.createInstance(CONSTANTS.Layer, options, paper)); 71 | } 72 | 73 | render() { 74 | const { className, canvasProps } = this.props; 75 | return ; 76 | } 77 | } 78 | 79 | export default (React.forwardRef( 80 | (props: any, ref) => , 81 | ): React.AbstractComponent); 82 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/CircleTool/CircleTool.component.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import * as ReactPaperJS from '@psychobolt/react-paperjs'; 4 | import Paper from 'paper'; 5 | 6 | import PathTool from '../shared/PathTool'; 7 | 8 | type ToolType = typeof Paper.Tool; 9 | type ToolEvent = typeof Paper.ToolEvent; 10 | 11 | const { Tool, PaperScope } = ReactPaperJS; 12 | 13 | type Props = { 14 | pathProps: { 15 | fillColor: string, 16 | strokeColor: string, 17 | }, 18 | innerRef: React.Ref 19 | }; 20 | 21 | const MOUSE_LEFT_CODE = 0; 22 | 23 | @PaperScope 24 | class CircleTool extends PathTool { 25 | static defaultProps = { 26 | ...PathTool.defaultProps, 27 | pathProps: { 28 | fillColor: 'white', 29 | strokeColor: 'black', 30 | }, 31 | } 32 | 33 | onMouseDown = (toolEvent: ToolEvent) => { 34 | const { pathProps, onMouseDown, onPathInit, paper } = this.props; 35 | if (toolEvent.event.button === MOUSE_LEFT_CODE) { 36 | const { Path, Color } = paper; 37 | const path = new Path.Circle({ 38 | center: toolEvent.point, 39 | radius: 1, 40 | fillColor: pathProps.selectedFillColor || new Color(0.9, 0.9, 1, 0.75), 41 | selected: true, 42 | }); 43 | this.path = path; 44 | onPathInit(path); 45 | } 46 | onMouseDown(toolEvent); 47 | } 48 | 49 | onMouseDrag = (toolEvent: ToolEvent) => { 50 | const { onMouseDrag } = this.props; 51 | if (toolEvent.event.buttons === 1) { 52 | const { path } = this; 53 | const scale = Math.abs(toolEvent.point.getDistance(path.position) / (path.bounds.width / 2)); 54 | if (scale > 0.1) { 55 | path.scale(scale); 56 | } 57 | } 58 | onMouseDrag(toolEvent); 59 | } 60 | 61 | onMouseUp = (event: ToolEvent) => { 62 | const { path } = this; 63 | const { pathProps, onMouseUp, onPathAdd } = this.props; 64 | if (path) { 65 | Object.assign(path, { 66 | selected: false, 67 | ...pathProps, 68 | }); 69 | onPathAdd(path); 70 | this.path = null; 71 | } 72 | onMouseUp(event); 73 | } 74 | 75 | render() { 76 | const { innerRef, ...rest } = this.props; 77 | return ( 78 | 85 | ); 86 | } 87 | } 88 | 89 | export default (React.forwardRef( 90 | (props, ref) => , 91 | ): React.AbstractComponent); 92 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/SegmentPathTool/SegmentPathTool.component.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import * as ReactPaperJS from '@psychobolt/react-paperjs'; 4 | import Paper from 'paper'; 5 | 6 | import PathTool from '../shared/PathTool'; 7 | 8 | const { Tool, PaperScope } = ReactPaperJS; 9 | 10 | type ToolEvent = typeof Paper.ToolEvent; 11 | 12 | type Props = { 13 | pathProps: { 14 | strokeColor: string, 15 | }, 16 | pathData: string, 17 | innerRef: React.Ref 18 | }; 19 | 20 | const MOUSE_LEFT_CODE = 0; 21 | 22 | @PaperScope 23 | class SegmentPathTool extends PathTool { 24 | static defaultProps = { 25 | ...PathTool.defaultProps, 26 | pathProps: { 27 | strokeColor: 'black', 28 | selected: true, 29 | }, 30 | }; 31 | 32 | onKeyUp = () => { 33 | const { path, onPathAdd } = this; 34 | if (path) { 35 | if (path.segments.length > 1) { 36 | onPathAdd(); 37 | } else { 38 | path.remove(); 39 | this.path = null; 40 | } 41 | } 42 | } 43 | 44 | onMouseDown = (toolEvent: ToolEvent) => { 45 | const { path } = this; 46 | if (toolEvent.event.button === MOUSE_LEFT_CODE && toolEvent.modifiers.shift) { 47 | if (!path) { 48 | this.pathInit(); 49 | this.props.onPathInit(path); 50 | } 51 | this.onSegmentAdd(toolEvent); 52 | } 53 | this.props.onMouseDown(toolEvent); 54 | } 55 | 56 | pathInit() { 57 | const { pathProps, pathData, paper } = this.props; 58 | const { Path } = paper; 59 | const path = new Path(pathProps); 60 | this.path = path; 61 | this.setPathData(pathData); 62 | } 63 | 64 | setPathData(pathData: string) { 65 | this.path.pathData = pathData; 66 | } 67 | 68 | onSegmentAdd(toolEvent: ToolEvent) { 69 | const { path } = this; 70 | path.add(toolEvent.point); 71 | this.props.onSegmentAdd(path.lastSegment, path); 72 | } 73 | 74 | onPathAdd = () => { 75 | const { path } = this; 76 | const { onPathAdd } = this.props; 77 | path.selected = false; 78 | onPathAdd(path); 79 | this.path = null; 80 | } 81 | 82 | render() { 83 | const { 84 | pathProps, 85 | onKeyUp, 86 | onMouseDown, 87 | onPathAdd, 88 | onSegmentAdd, 89 | onSegmentRemove, 90 | paper, 91 | innerRef, 92 | ...rest 93 | } = this.props; 94 | return ( 95 | 101 | ); 102 | } 103 | } 104 | 105 | export default (React.forwardRef( 106 | (props, ref) => , 107 | ): React.AbstractComponent); 108 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/README.md: -------------------------------------------------------------------------------- 1 | # React Paper.js Editor 2 | 3 | [![Stability](https://img.shields.io/badge/Stability-Experimental-Orange.svg)](https://nodejs.org/api/documentation.html#documentation_stability_index) 4 | [![npm](https://img.shields.io/npm/v/@psychobolt/react-paperjs-editor.svg)](https://www.npmjs.com/package/@psychobolt/react-paperjs-editor) 5 | [![Build Status](https://travis-ci.org/psychobolt/react-paperjs.svg?branch=master)](https://travis-ci.org/psychobolt/react-paperjs) 6 | [![codecov](https://codecov.io/gh/psychobolt/react-paperjs/branch/master/graph/badge.svg)](https://codecov.io/gh/psychobolt/react-paperjs) 7 | 8 | [![Dependencies Status](https://david-dm.org/psychobolt/react-paperjs/status.svg?path=packages/react-paperjs-editor)](https://david-dm.org/psychobolt/react-paperjs?path=packages/react-paperjs-editor) 9 | [![Dev Dependencies Status](https://david-dm.org/psychobolt/react-paperjs/dev-status.svg?path=packages/react-paperjs-editor)](https://david-dm.org/psychobolt/react-paperjs?path=packages/react-paperjs-editor&type=dev) 10 | [![Peer Dependencies Status](https://david-dm.org/psychobolt/react-paperjs/peer-status.svg?path=packages/react-paperjs-editor)](https://david-dm.org/psychobolt/react-paperjs?path=packages/react-paperjs-editor&type=peer) 11 | 12 | A library of common editor components for [React Paper.js](https://github.com/psychobolt/react-paperjs) 13 | 14 | ## Install 15 | 16 | ```sh 17 | npm install --save @psychobolt/react-paperjs-editor 18 | # or 19 | yarn add @psychobolt/react-paperjs-editor 20 | ``` 21 | 22 | ## Examples 23 | 24 | There are several [demos](https://psychobolt.github.io/react-paperjs/?path=/story/packages-react-paperjs-editor-setup). Also check out their [sources](https://github.com/psychobolt/react-paperjs/blob/master/stories/packages/react-paperjs-editor). 25 | 26 | ## Components 27 | 28 | - [CircleTool](https://github.com/psychobolt/react-paperjs/blob/master/packages/react-paperjs-editor/src/components/CircleTool/CircleTool.md) 29 | - [EllipseTool](https://github.com/psychobolt/react-paperjs/blob/master/packages/react-paperjs-editor/src/components/EllipseTool/EllipseTool.md) 30 | - [FreeformPathTool](https://github.com/psychobolt/react-paperjs/blob/master/packages/react-paperjs-editor/src/components/FreeformPathTool/FreeformPathTool.md) 31 | - [Grid](https://github.com/psychobolt/react-paperjs/blob/master/packages/react-paperjs-editor/src/components/Grid/Grid.md) 32 | - [LineTool](https://github.com/psychobolt/react-paperjs/blob/master/packages/react-paperjs-editor/src/components/LineTool/LineTool.md) 33 | - [PanAndZoom](https://github.com/psychobolt/react-paperjs/blob/master/packages/react-paperjs-editor/src/components/PanAndZoom/PanAndZoom.md) 34 | - [PolygonTool](https://github.com/psychobolt/react-paperjs/blob/master/packages/react-paperjs-editor/src/components/PolygonTool/PolygonTool.md) 35 | - [RectangleTool](https://github.com/psychobolt/react-paperjs/blob/master/packages/react-paperjs-editor/src/components/RectangleTool/RectangleTool.md) 36 | - [SegmentPathTool](https://github.com/psychobolt/react-paperjs/blob/master/packages/react-paperjs-editor/src/components/SegmentPathTool/SegmentPathTool.md) 37 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/RectangleTool/RectangleTool.component.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import * as ReactPaperJS from '@psychobolt/react-paperjs'; 4 | import Paper from 'paper'; 5 | 6 | import PathTool from '../shared/PathTool'; 7 | 8 | const { Tool, PaperScope } = ReactPaperJS; 9 | 10 | type ToolEvent = typeof Paper.ToolEvent; 11 | 12 | type Props = { 13 | pathProps: { 14 | fillColor: string, 15 | }, 16 | innerRef: React.Ref 17 | }; 18 | 19 | const MOUSE_LEFT_CODE = 0; 20 | 21 | @PaperScope 22 | class RectangleTool extends PathTool { 23 | static defaultProps = { 24 | ...PathTool.defaultProps, 25 | pathProps: { 26 | fillColor: 'white', 27 | strokeColor: 'black', 28 | }, 29 | } 30 | 31 | onMouseDown = (toolEvent: ToolEvent) => { 32 | const { pathProps, onMouseDown, onPathInit, paper } = this.props; 33 | if (toolEvent.event.button === MOUSE_LEFT_CODE) { 34 | const { Path, Color } = paper; 35 | const start = toolEvent.point; 36 | const path = new Path.Rectangle({ 37 | point: start, 38 | size: [1, 1], 39 | fillColor: pathProps.selectedFillColor || new Color(0.9, 0.9, 1, 0.75), 40 | selected: true, 41 | }); 42 | this.path = path; 43 | this.start = start; 44 | onPathInit(path); 45 | } 46 | onMouseDown(toolEvent); 47 | } 48 | 49 | onMouseDrag = (toolEvent: ToolEvent) => { 50 | const { onMouseDrag } = this.props; 51 | if (toolEvent.event.buttons === 1) { 52 | const { path, start } = this; 53 | const { bounds } = path; 54 | const offset = toolEvent.point.subtract(start); 55 | const width = Math.abs(offset.x); 56 | const height = Math.abs(offset.y); 57 | if (offset.x < 0) { 58 | bounds.left = toolEvent.point.x; 59 | bounds.right = start.x; 60 | } else { 61 | bounds.left = start.x; 62 | } 63 | if (offset.y > 0) { 64 | bounds.top = start.y; 65 | bounds.bottom = toolEvent.point.y; 66 | } else { 67 | bounds.top = toolEvent.point.y; 68 | } 69 | if (width > 0) { 70 | bounds.width = width; 71 | } 72 | if (height > 0) { 73 | bounds.height = height; 74 | } 75 | } 76 | onMouseDrag(toolEvent); 77 | } 78 | 79 | onMouseUp = (event: ToolEvent) => { 80 | const { path } = this; 81 | const { pathProps, onMouseUp, onPathAdd } = this.props; 82 | if (path) { 83 | Object.assign(path, { 84 | selected: false, 85 | ...pathProps, 86 | }); 87 | onPathAdd(path); 88 | this.path = null; 89 | this.start = null; 90 | } 91 | onMouseUp(event); 92 | } 93 | 94 | start: typeof Paper.Point; 95 | 96 | render() { 97 | const { innerRef, ...rest } = this.props; 98 | return ( 99 | 106 | ); 107 | } 108 | } 109 | 110 | export default (React.forwardRef( 111 | (props, ref) => , 112 | ): React.AbstractComponent); 113 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/EllipseTool/EllipseTool.component.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import * as ReactPaperJS from '@psychobolt/react-paperjs'; 4 | import Paper from 'paper'; 5 | 6 | import PathTool from '../shared/PathTool'; 7 | 8 | type ToolType = typeof Paper.Tool; 9 | type ToolEvent = typeof Paper.ToolEvent; 10 | 11 | const { Tool, PaperScope } = ReactPaperJS; 12 | 13 | type Props = { 14 | pathProps: { 15 | fillColor: string, 16 | }, 17 | innerRef: React.Ref 18 | }; 19 | 20 | const MOUSE_LEFT_CODE = 0; 21 | 22 | @PaperScope 23 | class EllipseTool extends PathTool { 24 | static defaultProps = { 25 | ...PathTool.defaultProps, 26 | pathProps: { 27 | fillColor: 'white', 28 | strokeColor: 'black', 29 | }, 30 | } 31 | 32 | onMouseDown = (toolEvent: ToolEvent) => { 33 | const { pathProps, onMouseDown, onPathInit, paper } = this.props; 34 | if (toolEvent.event.button === MOUSE_LEFT_CODE) { 35 | const { Path, Color } = paper; 36 | const start = toolEvent.point; 37 | const path = new Path.Ellipse({ 38 | point: start, 39 | size: [1, 1], 40 | fillColor: pathProps.selectedFillColor || new Color(0.9, 0.9, 1, 0.75), 41 | selected: true, 42 | }); 43 | 44 | this.path = path; 45 | this.start = start; 46 | onPathInit(this.path); 47 | } 48 | onMouseDown(toolEvent); 49 | } 50 | 51 | onMouseDrag = (toolEvent: ToolEvent) => { 52 | const { onMouseDrag } = this.props; 53 | if (toolEvent.event.buttons === 1) { 54 | const { path, start } = this; 55 | const { bounds } = path; 56 | const offset = toolEvent.point.subtract(start); 57 | const width = Math.abs(offset.x); 58 | const height = Math.abs(offset.y); 59 | if (offset.x < 0) { 60 | bounds.left = toolEvent.point.x; 61 | bounds.right = start.x; 62 | } else { 63 | bounds.left = start.x; 64 | } 65 | if (offset.y > 0) { 66 | bounds.top = start.y; 67 | bounds.bottom = toolEvent.point.y; 68 | } else { 69 | bounds.top = toolEvent.point.y; 70 | } 71 | if (width > 0) { 72 | bounds.width = width; 73 | } 74 | if (height > 0) { 75 | bounds.height = height; 76 | } 77 | } 78 | onMouseDrag(toolEvent); 79 | } 80 | 81 | onMouseUp = (event: ToolEvent) => { 82 | const { path } = this; 83 | const { pathProps, onMouseUp, onPathAdd } = this.props; 84 | if (path) { 85 | Object.assign(path, { 86 | selected: false, 87 | ...pathProps, 88 | }); 89 | onPathAdd(path); 90 | this.path = null; 91 | this.start = null; 92 | } 93 | onMouseUp(event); 94 | } 95 | 96 | start: typeof Paper.Point; 97 | 98 | render() { 99 | const { innerRef, ...rest } = this.props; 100 | return ( 101 | 108 | ); 109 | } 110 | } 111 | 112 | export default (React.forwardRef( 113 | (props, ref) => , 114 | ): React.AbstractComponent); 115 | -------------------------------------------------------------------------------- /src/Paper.provider.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import { defaultMemoize } from 'reselect'; 4 | import Paper from 'paper'; 5 | 6 | import PaperRenderer from './Paper.renderer'; 7 | import { CONSTANTS } from './Paper.types'; 8 | import { PaperScopeContext } from './hoc/PaperScope'; 9 | 10 | type CanvasProps = { 11 | onWheel: (event: SyntheticWheelEvent) => any 12 | }; 13 | 14 | export type EventHandler = (event: any) => any; 15 | export type KeyEventHandler = (event: typeof Paper.KeyEvent) => any; 16 | export type MouseEventHandler = (event: typeof Paper.MouseEvent) => any; 17 | export type ToolEventHandler = (event: typeof Paper.ToolEvent) => any; 18 | 19 | type ViewProps = { 20 | onKeyDown: KeyEventHandler, 21 | onKeyUp: KeyEventHandler, 22 | onMouseDown: MouseEventHandler, 23 | onMouseDrag: MouseEventHandler, 24 | onMouseUp: MouseEventHandler, 25 | zoom: number, 26 | center: {} | number[], 27 | }; 28 | 29 | type ScopedProps

= (paper: typeof Paper.PaperScope) => P; 30 | 31 | type NestedProps

= P | ScopedProps

; 32 | 33 | export type Props = { 34 | renderer?: typeof PaperRenderer, 35 | innerRef: Object, 36 | viewProps: NestedProps, 37 | canvasProps: NestedProps, 38 | mergeProps: () => any, 39 | children?: React.Node 40 | }; 41 | 42 | type State = { 43 | paper: typeof Paper.PaperScope, 44 | viewProps?: NestedProps, 45 | canvasProps?: NestedProps 46 | }; 47 | 48 | export function getProps(scope: typeof Paper.PaperScope, props: NestedProps): any { 49 | if (typeof props === 'function') { 50 | return props(scope); 51 | } 52 | return props || {}; 53 | } 54 | 55 | const getMergeProps = () => (state, props, scope) => ({ 56 | ...getProps(scope, props), 57 | ...getProps(scope, state), 58 | }); 59 | 60 | export default (Container => class PaperProvider 61 | extends React.Component { 62 | mergeContainerProps = defaultMemoize(getMergeProps()); 63 | 64 | mergeViewProps = defaultMemoize(getMergeProps()); 65 | 66 | mergeCanvasProps = defaultMemoize(getMergeProps()); 67 | 68 | static defaultProps = { 69 | renderer: PaperRenderer, 70 | children: null, 71 | } 72 | 73 | constructor(props: Props) { 74 | super(props); 75 | const { renderer: Renderer = PaperRenderer } = props; 76 | this.renderer = new Renderer(); 77 | this.state = { 78 | paper: this.renderer.createInstance(CONSTANTS.PaperScope, {}, Paper), 79 | mergeProps: props.mergeProps // eslint-disable-line react/no-unused-state 80 | || (mergeProps => this.setState(state => mergeProps(state, props))), 81 | }; 82 | } 83 | 84 | renderer: PaperRenderer; 85 | 86 | render() { 87 | const { innerRef, children, viewProps, canvasProps, ...rest } = this.props; 88 | const { viewProps: viewState, canvasProps: canvasState, ...state } = this.state; 89 | return ( 90 | 97 | 98 | {children} 99 | 100 | 101 | ); 102 | } 103 | }: React.ComponentType => React.AbstractComponent); 104 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development Guide 2 | 3 | ## Setup 4 | 5 | Install the latest [Node JS LTS](https://nodejs.org/) and [Yarn](https://yarnpkg.com) and simply run ```yarn bootstrap``` command in the root project directory. 6 | 7 | ## Local development 8 | 9 | During development, 10 | ```sh 11 | yarn start # watch, build, and serves packages 12 | ``` 13 | 14 | ## Including NPM packages 15 | 16 | ```sh 17 | yarn add --dev # for dev tools, story dependencies, libraries to be bundled 18 | yarn add [--peer] # for external dependencies (Note: Include in externals from rollup.config.common.js whenever update) 19 | yarn lerna add [--dev] packages/] # Add/link a package to all or specific local package(s). See section: Including local packages 20 | ``` 21 | 22 | ## Including local packages 23 | 24 | This boilerplate supports [Monorepo](https://danluu.com/monorepo/) configurations out of the box and will watch, build, serve any local packages. Each package should have ```src/index.js``` entry file. 25 | 26 | By default, local packages are [independently](https://github.com/psychobolt/react-rollup-boilerplate/blob/master/lerna.json#L6) versioned. You may import your own repos with Lerna or create your own sub-packages using NPM: 27 | 28 | ```sh 29 | yarn lerna import # import a repository to packages/ 30 | # or 31 | mkdir packages/ && cd && yarn init 32 | ``` 33 | 34 | See Lerna's offical [readme](https://github.com/lerna/lerna#readme) for a configuration and usage guide. 35 | 36 | > You can also give alias to source files of the packages in order to work with Visual Studio Code's Intellisense. See [jsconfig.json](https://github.com/psychobolt/react-rollup-boilerplate/blob/master/jsconfig.json) and [usage](https://code.visualstudio.com/docs/languages/jsconfig#_using-webpack-aliases). 37 | 38 | ## Main Package 39 | 40 | By default, the ```lerna.json``` defines the main package at the [root](https://github.com/psychobolt/react-rollup-boilerplate/blob/master/lerna.json#L3). You may opt-out of this configuration manually, by removing its settings and any alias references to its directory or package. 41 | 42 | > Note, the main package has one limitation: it cannot include any non-published packages. 43 | 44 | ## Static Types 45 | 46 | ```sh 47 | yarn flow-typed-install # clean & install flow definitions 48 | yarn flow-typed-update # downloads and updates new flow definitions 49 | yarn flow # performs type checking on files 50 | ``` 51 | 52 | ## Lint 53 | 54 | ```sh 55 | yarn lint # runs linter to detect any style issues (css & js) 56 | yarn lint:css # lint only css 57 | yarn lint:js # lint only js 58 | yarn lint:js --fix # attempts to fix js lint issues 59 | ``` 60 | 61 | ## Test 62 | 63 | ```sh 64 | yarn test # runs functional/unit tests for all packages 65 | ``` 66 | 67 | > Configurable with .project file, supports the [PACKAGES](#packages) variable. You can also inspect all tests in debug mode within Visual Studio Code. 68 | 69 | ## Coverage 70 | 71 | Coverage will be uploaded to your [codecov](https://codecov.io/) account, individually for packages by using each package's name as a [flag](https://docs.codecov.io/docs/flags). By default, coverage is configured to utilize a configuration from codecov-config branch (for [example](https://github.com/psychobolt/react-rollup-boilerplate/tree/codecov-config)). However, you may opt out that setting and configure codecov.yml in the master branch. 72 | 73 | ```sh 74 | yarn codecov # Runs tests and upload coverage for all packages 75 | ``` 76 | 77 | > Configurable with .project file, supports the [PACKAGES](#packages) variable. 78 | 79 | ## Other scripts 80 | 81 | ```sh 82 | 83 | yarn build # builds sources for prod and dev 84 | yarn build:dev # builds sources for development 85 | yarn build:prod # builds sources for production 86 | 87 | yarn watch # watches dev builds 88 | yarn dist # builds all packages and publishes to npm 89 | ``` 90 | 91 | > Configurable with .project file, supports the [PACKAGES](#packages) variable. 92 | 93 | ## Environment Variables 94 | 95 | ### PACKAGES 96 | 97 | Some scripts optionally allow the environment variable to specific local packages(s) (in Glob format) for running scripts e.g. ```PACKAGES=default-export,package-* yarn test``` This environment variable will override the .projects config. -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/PolygonTool/PolygonTool.component.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import * as ReactPaperJS from '@psychobolt/react-paperjs'; 4 | import Paper from 'paper'; 5 | 6 | import PathTool from '../shared/PathTool'; 7 | 8 | const { Tool, PaperScope } = ReactPaperJS; 9 | 10 | type ToolType = typeof Paper.Tool; 11 | type ToolEvent = typeof Paper.ToolEvent; 12 | type Segment = typeof Paper.Segment; 13 | 14 | type Props = { 15 | pathProps: { 16 | strokeColor: string, 17 | }, 18 | pathData: string, 19 | innerRef: React.Ref 20 | }; 21 | 22 | const MOUSE_LEFT_CODE = 0; 23 | 24 | @PaperScope 25 | class PolygonTool extends PathTool { 26 | static defaultProps = { 27 | ...PathTool.defaultProps, 28 | pathProps: { 29 | strokeColor: 'black', 30 | selected: true, 31 | }, 32 | }; 33 | 34 | componentDidUpdate() { 35 | const { path, points, props } = this; 36 | const { pathProps, pathData } = props; 37 | if (path) { 38 | this.setPathData(pathData); 39 | Object.assign(path, pathProps); 40 | } else if (points) { 41 | this.pathInit(); 42 | } 43 | } 44 | 45 | onMouseDown = (toolEvent: ToolEvent) => { 46 | if (toolEvent.event.button === MOUSE_LEFT_CODE) { 47 | const { path } = this; 48 | if (!path) { 49 | this.pathInit(); 50 | this.props.onPathInit(path); 51 | } 52 | if (this.selectedSegment == null) { 53 | this.onSegmentAdd(toolEvent); 54 | } else { 55 | this.onPathAdd(); 56 | } 57 | } 58 | this.props.onMouseDown(toolEvent); 59 | } 60 | 61 | pathInit() { 62 | const { pathProps, pathData, paper } = this.props; 63 | const { Path } = paper; 64 | const path = new Path(pathProps); 65 | this.path = path; 66 | this.setPathData(pathData); 67 | } 68 | 69 | setPathData(pathData: string) { 70 | const { path } = this; 71 | this.removeBounds(); 72 | path.pathData = pathData; 73 | path.segments.forEach(segment => this.createBounds(segment)); 74 | } 75 | 76 | onSegmentAdd(toolEvent: ToolEvent) { 77 | const { path } = this; 78 | path.add(toolEvent.point); 79 | const segment = path.lastSegment; 80 | this.createBounds(segment); 81 | this.props.onSegmentAdd(segment, path); 82 | } 83 | 84 | onPathAdd() { 85 | const { selectedSegment, path, points } = this; 86 | const { onSegmentRemove, onPathAdd } = this.props; 87 | const { index } = selectedSegment; 88 | const segments = path.removeSegments(0, index); 89 | if (segments.length) { 90 | onSegmentRemove(segments, path); 91 | } 92 | path.closed = true; 93 | path.selected = false; 94 | onPathAdd(path); 95 | this.path = null; 96 | this.selectedSegment = null; 97 | if (points) { 98 | points.remove(); 99 | } 100 | } 101 | 102 | createBounds(segment: Segment) { 103 | const { paper } = this.props; 104 | const { Path, Group, project } = paper; 105 | const { path, points } = this; 106 | if (!points) { 107 | this.points = new Group(); 108 | project.layers.$$metadata.addChild(this.points); 109 | } 110 | const bounds = new Path.Circle({ 111 | center: segment.point, 112 | radius: 7, 113 | fillColor: 'white', 114 | opacity: 0, 115 | }); 116 | bounds.on('mousedown', () => { 117 | if (!path.closed 118 | && !path.lastSegment.point.equals(bounds.position) 119 | && path.contains(bounds.position)) { 120 | this.selectedSegment = segment; 121 | } 122 | }); 123 | this.points.addChild(bounds); 124 | } 125 | 126 | removeBounds() { 127 | if (this.points) { 128 | this.points.remove(); 129 | this.points = null; 130 | } 131 | } 132 | 133 | points: typeof Paper.Points; 134 | 135 | selectedSegment: Segment; 136 | 137 | render() { 138 | const { 139 | pathProps, onMouseDown, onPathAdd, onSegmentAdd, onSegmentRemove, paper, innerRef, ...rest 140 | } = this.props; 141 | return ( 142 | 147 | ); 148 | } 149 | } 150 | 151 | export default (React.forwardRef( 152 | (props, ref) => , 153 | ): React.AbstractComponent); 154 | -------------------------------------------------------------------------------- /stories/packages/react-paperjs-editor/Tool/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PaperContainer } from '@psychobolt/react-paperjs'; 3 | 4 | import { LineTool, FreeformPathTool, PolygonTool, RectangleTool, CircleTool, SegmentPathTool, EllipseTool } from '@psychobolt/react-paperjs-editor'; 5 | import LineToolReadme from 'packages/react-paperjs-editor/src/components/LineTool/LineTool.md'; 6 | import FreeformPathToolReadme from 'packages/react-paperjs-editor/src/components/FreeformPathTool/FreeformPathTool.md'; 7 | import SegmentPathToolReadme from 'packages/react-paperjs-editor/src/components/SegmentPathTool/SegmentPathTool.md'; 8 | import RectangleToolReadme from 'packages/react-paperjs-editor/src/components/RectangleTool/RectangleTool.md'; 9 | import CircleToolReadme from 'packages/react-paperjs-editor/src/components/CircleTool/CircleTool.md'; 10 | import PolygonToolReadme from 'packages/react-paperjs-editor/src/components/PolygonTool/PolygonTool.md'; 11 | import EllipseToolReadme from 'packages/react-paperjs-editor/src/components/EllipseTool/EllipseTool.md'; 12 | 13 | import { ref } from '../shared'; 14 | import styles from './Tool.styles'; 15 | 16 | function onPathAdd(path) { 17 | console.log(path); // eslint-disable-line no-console 18 | } 19 | 20 | export default { 21 | title: 'packages/react-paperjs-editor/Tool', 22 | }; 23 | 24 | export const Line = () => ( 25 |

26 |
Click and drag to draw a line
27 | 28 | 29 | 30 |
31 | ); 32 | 33 | Line.parameters = { 34 | docs: { 35 | page: LineToolReadme, 36 | }, 37 | }; 38 | 39 | export const FreeformPath = () => ( 40 |
41 |
Click and drag to freeform lines.
42 | 43 | 44 | 45 |
46 | ); 47 | 48 | FreeformPath.parameters = { 49 | docs: { 50 | page: FreeformPathToolReadme, 51 | }, 52 | }; 53 | 54 | export const SegmentPath = () => ( 55 |
56 |
57 |

Click start to begin.

58 | 59 |

Hold down shift, then left click to plot points to form a segment.

60 |

Release shift to complete segment path.

61 |
62 | 63 | 64 | 65 |
66 | ); 67 | 68 | SegmentPath.parameters = { 69 | docs: { 70 | page: SegmentPathToolReadme, 71 | }, 72 | }; 73 | 74 | export const Rectangle = () => ( 75 |
76 |
Click and drag to create rectangle shapes.
77 | 78 | 79 | 80 |
81 | ); 82 | 83 | Rectangle.parameters = { 84 | docs: { 85 | page: RectangleToolReadme, 86 | }, 87 | }; 88 | 89 | export const Circle = () => ( 90 |
91 |
Click and drag to create circle shapes.
92 | 93 | 94 | 95 |
96 | ); 97 | 98 | Circle.parameters = { 99 | docs: { 100 | page: CircleToolReadme, 101 | }, 102 | }; 103 | 104 | export const Ellipse = () => ( 105 |
106 |
Click and drag to create ellipse shapes.
107 | 108 | 109 | 110 |
111 | ); 112 | 113 | Ellipse.parameters = { 114 | docs: { 115 | page: EllipseToolReadme, 116 | }, 117 | }; 118 | 119 | export const Polygon = () => ( 120 |
121 |
122 |

123 | {'Click anywhere to plot points and to create a shape. '} 124 |

125 |

Click near points to close the path and prune dangling points.

126 |
127 | 128 | 129 | 130 |
131 | ); 132 | 133 | Polygon.parameters = { 134 | docs: { 135 | page: PolygonToolReadme, 136 | }, 137 | }; 138 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@psychobolt/react-paperjs", 3 | "version": "1.0.3", 4 | "description": "React Fiber renderer and component container for Paper.js", 5 | "main": "./dist/index.js", 6 | "repository": "https://github.com/psychobolt/react-paperjs.git", 7 | "author": "psychobolt", 8 | "license": "MIT", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "scripts": { 13 | "flow-typed-update": "cross-env BABEL_ENV=commonjs babel-node flow-typed-update.js", 14 | "flow-typed-install": "rimraf flow-typed/npm && npm run flow-typed-update", 15 | "build:dev": "cross-env BABEL_ENV=rollup rollup -c rollup.config.dev.js", 16 | "build:prod": "cross-env BABEL_ENV=rollup rollup -c rollup.config.prod.js", 17 | "clean": "rimraf dist/index.*.js dist/*-*.*.js packages/*/dist/index.*.js packages/*/dist/*-*.*.js", 18 | "build": "npm run clean && npm run build:dev && npm run build:prod", 19 | "watch": "npm run build:dev -- --w", 20 | "start": "concurrently \"npm:watch\" \"npm:storybook\"", 21 | "test": "cross-env NODE_OPTIONS=\"-r @babel/register\" BABEL_ENV=test jest --passWithNoTests", 22 | "lint": "npm run lint:css && npm run lint:js", 23 | "lint:css": "stylelint ./src/**/*.js", 24 | "lint:js": "cross-env BABEL_ENV=commonjs eslint \"**/*.js\"", 25 | "storybook": "start-storybook", 26 | "build-storybook": "build-storybook", 27 | "bootstrap": "yarn install && lerna bootstrap", 28 | "dist": "npm run build && lerna publish", 29 | "codecov": "cross-env BABEL_ENV=commonjs babel-node codecov.js" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.14.0", 33 | "@babel/node": "^7.13.13", 34 | "@babel/plugin-proposal-class-properties": "^7.13.0", 35 | "@babel/plugin-proposal-decorators": "^7.13.15", 36 | "@babel/plugin-proposal-do-expressions": "^7.14.0", 37 | "@babel/plugin-proposal-export-default-from": "^7.12.13", 38 | "@babel/plugin-proposal-export-namespace-from": "^7.12.13", 39 | "@babel/plugin-proposal-json-strings": "^7.13.8", 40 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", 41 | "@babel/plugin-proposal-numeric-separator": "^7.12.13", 42 | "@babel/plugin-proposal-optional-chaining": "^7.13.12", 43 | "@babel/plugin-proposal-private-methods": "^7.13.0", 44 | "@babel/plugin-proposal-private-property-in-object": "^7.14.0", 45 | "@babel/plugin-proposal-throw-expressions": "^7.12.13", 46 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 47 | "@babel/plugin-transform-modules-commonjs": "^7.14.0", 48 | "@babel/preset-env": "^7.14.0", 49 | "@babel/preset-flow": "^7.13.13", 50 | "@babel/preset-react": "^7.13.13", 51 | "@babel/register": "^7.13.16", 52 | "@lerna/project": "^4.0.0", 53 | "@rollup/plugin-alias": "^3.1.2", 54 | "@rollup/plugin-babel": "^5.3.0", 55 | "@rollup/plugin-commonjs": "^18.0.0", 56 | "@rollup/plugin-node-resolve": "^11.2.1", 57 | "@storybook/addon-actions": "^6.2.9", 58 | "@storybook/addon-console": "^1.2.3", 59 | "@storybook/addon-essentials": "^6.2.9", 60 | "@storybook/addon-links": "^6.2.9", 61 | "@storybook/cli": "^6.2.9", 62 | "@storybook/react": "^6.2.9", 63 | "@storybook/theming": "^6.2.9", 64 | "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1", 65 | "babel-core": "^7.0.0-bridge.0", 66 | "babel-eslint": "10.1.0", 67 | "babel-jest": "^26.6.3", 68 | "babel-loader": "^8.2.2", 69 | "babel-plugin-dynamic-import-node": "^2.3.3", 70 | "babel-plugin-lodash": "^3.3.4", 71 | "babel-plugin-module-resolver": "^4.1.0", 72 | "babel-plugin-styled-components": "^1.12.0", 73 | "cash-true": "^0.0.2", 74 | "codecov": "^3.8.1", 75 | "concurrently": "^6.0.2", 76 | "cross-env": "^7.0.3", 77 | "cross-spawn": "^7.0.3", 78 | "enzyme": "^3.11.0", 79 | "eslint": "^7.25.0", 80 | "eslint-config-airbnb": "^18.2.1", 81 | "eslint-config-react-app": "^6.0.0", 82 | "eslint-import-resolver-babel-module": "^5.3.1", 83 | "eslint-plugin-flowtype": "^5.7.2", 84 | "eslint-plugin-import": "^2.22.1", 85 | "eslint-plugin-jest": "^24.3.6", 86 | "eslint-plugin-jsx-a11y": "^6.4.1", 87 | "eslint-plugin-react": "^7.23.2", 88 | "flow-bin": "^0.150.0", 89 | "flow-typed": "^3.3.1", 90 | "glob": "^7.1.6", 91 | "is-class": "^0.0.9", 92 | "jest": "^26.6.3", 93 | "jest-styled-components": "^7.0.4", 94 | "lerna": "^4.0.0", 95 | "lodash": "^4.17.21", 96 | "micromatch": "^4.0.4", 97 | "paper": "^0.12.15", 98 | "prop-types": "^15.7.2", 99 | "raf": "^3.4.1", 100 | "raw-loader": "^4.0.2", 101 | "re-resizable": "^6.9.0", 102 | "react": "^17.0.2", 103 | "react-cache": "^2.0.0-alpha.1", 104 | "react-dom": "^17.0.2", 105 | "react-reconciler": "0.26.2", 106 | "rimraf": "^3.0.2", 107 | "rollup": "2.46.0", 108 | "rollup-plugin-terser": "^7.0.2", 109 | "slash": "3.0.0", 110 | "source-map-loader": "1.1.0", 111 | "styled-components": "^5.2.3", 112 | "stylelint": "^13.13.1", 113 | "stylelint-config-recommended": "^5.0.0", 114 | "stylelint-config-styled-components": "^0.1.1", 115 | "stylelint-processor-styled-components": "^1.10.0" 116 | }, 117 | "dependencies": { 118 | "react-is": "^17.0.2" 119 | }, 120 | "peerDependencies": { 121 | "paper": "^0.12.4", 122 | "react": "^17.0.2", 123 | "react-dom": "^17.0.2" 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Paper.js 2 | 3 | [![npm](https://img.shields.io/npm/v/@psychobolt/react-paperjs.svg)](https://www.npmjs.com/package/@psychobolt/react-paperjs) 4 | [![Build Status](https://travis-ci.com/psychobolt/react-paperjs.svg?branch=master)](https://travis-ci.com/psychobolt/react-paperjs) 5 | [![codecov](https://codecov.io/gh/psychobolt/react-paperjs/branch/master/graph/badge.svg)](https://codecov.io/gh/psychobolt/react-paperjs) 6 | 7 | [![Dependencies Status](https://david-dm.org/psychobolt/react-paperjs.svg)](https://david-dm.org/psychobolt/react-paperjs) 8 | [![Dev Dependencies Status](https://david-dm.org/psychobolt/react-paperjs/dev-status.svg)](https://david-dm.org/psychobolt/react-paperjs?type=dev) 9 | [![Peer Dependencies Status](https://david-dm.org/psychobolt/react-paperjs/peer-status.svg)](https://david-dm.org/psychobolt/react-paperjs?type=peer) 10 | 11 | React fiber renderer and component container for [Paper.js](http://paperjs.org/). 12 | 13 | ## Install 14 | 15 | > Recommended: Paper 0.12.x, React, React DOM 17.x. 16 | 17 | ```sh 18 | npm install --save @psychobolt/react-paperjs 19 | # or 20 | yarn add @psychobolt/react-paperjs 21 | ``` 22 | 23 | ## Examples 24 | 25 | There are several [demos](https://psychobolt.github.io/react-paperjs). Also check out their [sources](https://github.com/psychobolt/react-paperjs/blob/master/stories). Here is one to get you started: 26 | 27 | ```jsx 28 | import React from 'react'; 29 | import { PaperContainer, Circle, Layer } from '@psychobolt/react-paperjs' 30 | 31 | const Shapes = () => ; 32 | 33 | const App = (props) => ( 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | ); 43 | 44 | export default App; 45 | ``` 46 | 47 | ## Components 48 | 49 | Common usage with [PaperContainer](#papercontainer) and its default [renderer](#paperrenderer). 50 | 51 | ### PaperContainer 52 | 53 | Provide and creates [Paper Scope](http://paperjs.org/reference/paperscope/) context. To access Paper Scope, you may use the provided [HOC](#paper-scope). All children are rendered into its canvas with [PaperRenderer](#paperrenderer) by default. 54 | 55 | #### Props 56 | 57 | ##### `renderer?: Renderer` 58 | 59 | The default is [PaperRenderer](#paperrenderer). Alternatively, you can [extend](#extension-example) and pass in your own. 60 | 61 | ##### `canvasProps?: {} | (paper) => ({})` 62 | 63 | Props to be passed to ``````. Alternatively, you can provide a function that returns new props. 64 | 65 | ##### `viewProps?: {} | (paper) => ({})` 66 | 67 | Props to be passed to the [View](http://paperjs.org/reference/view/). Alternatively, you can provide a function that returns new props. 68 | 69 | ##### `onMount?: (paper) => myCallback(paper)` 70 | 71 | Callback on container mount. 72 | 73 | ##### `className?: string` 74 | 75 | Canvas element class attribute. 76 | 77 | ### Paper 78 | 79 | Refer supported Paper [types](https://github.com/psychobolt/react-paperjs/blob/master/src/Paper.types.js). All props are passed to the type constructor. 80 | 81 | ## API 82 | 83 | ### PaperRenderer 84 | 85 | Currently a synchronous but extensible implementation. 86 | 87 | #### Members 88 | 89 | ##### `defaultHostConfig: {}` 90 | 91 | The host config that is passed into React Reconciler by default. __This should not be mutated.__ Instead, extend [PaperRenderer](#paperrenderer) with a ```getHostConfig``` function. 92 | 93 | ##### `defaultTypes: { [type: string]: (props: {}, paper: Paper) => Object}` 94 | 95 | A mapping of types with their instance factory method. __This should not be mutated.__ Instead, extend [PaperRenderer](#paperrenderer) with a ```getInstanceFactory``` function. 96 | 97 | #### Extension Example 98 | 99 | ```js 100 | import React from 'React'; 101 | import { PaperContainer, PaperRenderer } from '@psychobolt/react-paperjs' 102 | 103 | import MyCustomStencil, { TYPE_NAME as MyCustomStencilComponent } from './MyCustomStencil'; 104 | 105 | class MyPaperRenderer extends PaperRenderer { 106 | getInstanceFactory() { 107 | return { 108 | ...this.defaultTypes, /* 109 | refer to default types 110 | see https://github.com/psychobolt/react-paperjs/blob/master/src/Paper.types.js#L42 111 | */ 112 | [MyCustomStencilComponent]: (props, paper) => new MyCustomStencil(props), 113 | }; 114 | } 115 | } 116 | 117 | const App = (props) => ( 118 | 119 | 120 | 121 | ); 122 | 123 | export default App; 124 | ``` 125 | 126 | The above code adds a custom Component Type to the renderer's instance factory. Then the component can be rendered inside the container. 127 | 128 | ## Higher-order Components 129 | 130 | #### Paper Scope 131 | 132 | Injects Paper Scope as component prop 'paper'. 133 | 134 | Example usage: 135 | ```jsx 136 | import React from 'react'; 137 | 138 | import { PaperScope, Circle } from '@psychobolt/react-paperjs'; 139 | 140 | export default @PaperScope class Scene { 141 | render() { 142 | const { paper } = this.props; 143 | return ; 144 | } 145 | } 146 | ``` 147 | 148 | As an alternative, you can use a helper function: 149 | ```jsx 150 | import React from 'react'; 151 | 152 | import { renderWithPaperScope, Circle } from '@psychobolt/react-paperjs'; 153 | 154 | export default class Scene { 155 | render() { 156 | return renderWithPaperScope(paper => ); 157 | } 158 | } 159 | ``` 160 | 161 | ## Extensions 162 | 163 | If you're interested in editor components for React Paper JS, you can checkout another [library](https://www.npmjs.com/package/@psychobolt/react-paperjs-editor) that's work in progress. 164 | 165 | ## Development Guide 166 | 167 | Please see [DEVELOPMENT.md](DEVELOPMENT.md) 168 | -------------------------------------------------------------------------------- /packages/react-paperjs-editor/src/components/PanAndZoom/PanAndZoom.component.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import * as ReactPaperJS from '@psychobolt/react-paperjs'; 4 | import type { EventHandler } from '@psychobolt/react-paperjs'; 5 | import Paper from 'paper'; 6 | 7 | const { PaperScope, getProps } = ReactPaperJS; 8 | 9 | type PaperScopeType = typeof Paper.PaperScope; 10 | type KeyEvent = typeof Paper.KeyEvent; 11 | 12 | type DefaultProps = { 13 | onPanEnabled?: () => any, 14 | onPanDisabled?: () => any, 15 | onZoom?: (level: number) => any, 16 | zoomLevel?: number, 17 | }; 18 | 19 | type Props = { 20 | center: Object | number[], 21 | paper: PaperScopeType, 22 | mergeProps: (state: {}, props?: {}) => {}, 23 | children: any, 24 | } & DefaultProps; 25 | 26 | type State = { 27 | draggable: boolean, 28 | dragStart: ?Object, 29 | }; 30 | 31 | function add(num1, num2) { 32 | return ((num1 * 10) + (num2 * 10)) / 10; 33 | } 34 | 35 | function callAllHandlers(handlers: EventHandler[] = []) { 36 | return event => handlers.forEach(handler => handler && handler(event)); 37 | } 38 | 39 | export default @PaperScope class PanAndScroll extends React.Component { 40 | static defaultProps: DefaultProps = { 41 | zoomLevel: 1, 42 | onPanEnabled: () => {}, 43 | onPanDisabled: () => {}, 44 | onZoom: () => {}, 45 | }; 46 | 47 | constructor(props: Props) { 48 | super(props); 49 | this.state = { 50 | draggable: false, 51 | dragStart: null, 52 | }; 53 | } 54 | 55 | componentDidMount() { 56 | const { paper, zoomLevel, center, mergeProps } = this.props; 57 | mergeProps((state, props) => { 58 | const { onWheel, ...canvasProps } = getProps(paper, props.canvasProps); 59 | const { 60 | onKeyDown, onKeyUp, onMouseDown, onMouseDrag, onMouseUp, ...viewProps 61 | } = getProps(paper, props.viewProps); 62 | return { 63 | canvasProps: { 64 | ...canvasProps, 65 | onWheel: callAllHandlers([onWheel, this.onWheel]), 66 | 'drag-state': 'disabled', 67 | }, 68 | viewProps: { 69 | ...viewProps, 70 | onKeyDown: callAllHandlers([onKeyDown, this.onKeyDown]), 71 | onKeyUp: callAllHandlers([onKeyUp, this.onKeyUp]), 72 | onMouseDown: callAllHandlers([onMouseDown, this.onMouseDown]), 73 | onMouseDrag: callAllHandlers([onMouseDrag, this.onMouseDrag]), 74 | onMouseUp: callAllHandlers([onMouseUp, this.onMouseUp]), 75 | zoom: zoomLevel, 76 | center, 77 | }, 78 | }; 79 | }); 80 | } 81 | 82 | onWheel: SyntheticWheelEvent => void = ({ deltaY }) => { 83 | const { onZoom, mergeProps } = this.props; 84 | mergeProps((state, props) => { 85 | let { zoom } = state.viewProps; 86 | if (deltaY < 0) { 87 | zoom = add(zoom, 0.1); 88 | if (onZoom) onZoom(zoom); 89 | return { 90 | viewProps: { 91 | ...props.viewProps, 92 | ...state.viewProps, 93 | zoom, 94 | }, 95 | }; 96 | } 97 | if (deltaY > 0 && zoom > 0.1) { 98 | zoom = add(zoom, -0.1); 99 | if (onZoom) onZoom(zoom); 100 | return { 101 | viewProps: { 102 | ...props.viewProps, 103 | ...state.viewProps, 104 | zoom, 105 | }, 106 | }; 107 | } 108 | return null; 109 | }); 110 | } 111 | 112 | onKeyDown: KeyEvent => void = ({ key }) => { 113 | const { draggable } = this.state; 114 | if (key === 'space' && !draggable) { 115 | const { onPanEnabled, mergeProps } = this.props; 116 | mergeProps((state, props) => ({ 117 | ...state, 118 | canvasProps: { 119 | ...props.canvasProps, 120 | ...state.canvasProps, 121 | 'drag-state': 'enabled', 122 | }, 123 | })); 124 | this.setState({ draggable: true }); 125 | if (onPanEnabled) onPanEnabled(); 126 | } 127 | } 128 | 129 | onKeyUp: KeyEvent => void = ({ key }) => { 130 | if (key === 'space') { 131 | const { onPanDisabled, mergeProps } = this.props; 132 | mergeProps((state, props) => ({ 133 | ...state, 134 | canvasProps: { 135 | ...props.canvasProps, 136 | ...state.canvasProps, 137 | 'drag-state': 'disabled', 138 | }, 139 | })); 140 | this.setState({ draggable: false }); 141 | if (onPanDisabled) onPanDisabled(); 142 | } 143 | } 144 | 145 | onMouseDown: KeyEvent => void = ({ point }) => { 146 | const { draggable, dragStart } = this.state; 147 | if (draggable && !dragStart) { 148 | const { mergeProps } = this.props; 149 | mergeProps((state, props) => ({ 150 | ...state, 151 | canvasProps: { 152 | ...props.canvasProps, 153 | ...state.canvasProps, 154 | 'drag-state': 'dragging', 155 | }, 156 | })); 157 | this.setState({ dragStart: point }); 158 | } 159 | } 160 | 161 | onMouseUp: () => void = () => { 162 | const { dragStart, draggable } = this.state; 163 | if (dragStart) { 164 | if (draggable) { 165 | const { mergeProps } = this.props; 166 | mergeProps((state, props) => ({ 167 | ...state, 168 | canvasProps: { 169 | ...props.canvasProps, 170 | ...state.canvasProps, 171 | 'drag-state': 'enabled', 172 | }, 173 | })); 174 | } 175 | this.setState({ dragStart: null }); 176 | } 177 | } 178 | 179 | onMouseDrag: KeyEvent => void = ({ point }) => { 180 | const { mergeProps, paper } = this.props; 181 | const { draggable, dragStart } = this.state; 182 | mergeProps((state, props) => { 183 | if (dragStart) { 184 | return { 185 | viewProps: { 186 | ...props.viewProps, 187 | ...state.viewProps, 188 | center: 189 | paper.view.center 190 | .add(point.subtract(dragStart) 191 | .multiply(0.5)), 192 | }, 193 | }; 194 | } 195 | return null; 196 | }); 197 | if (draggable) { 198 | this.setState({ dragStart: point }); 199 | } 200 | } 201 | 202 | render(): React.Node { 203 | const { children } = this.props; 204 | return children; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/Paper.renderer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Reconciler from 'react-reconciler'; 3 | import Paper from 'paper'; 4 | 5 | import TYPES, { type Types } from './Paper.types'; 6 | import { diffProps, updateProps } from './Paper.component'; 7 | 8 | type Props = { 9 | pathData?: string, 10 | children?: any, 11 | }; 12 | 13 | type CreateInstance = (type: string, props: Props, paper: typeof Paper.PaperScope) => any; 14 | 15 | export function getTypes(instanceFactory: Types): CreateInstance { 16 | return (type: string, { children, pathData, ...rest }: Props, paper: typeof Paper.PaperScope) => { 17 | const TYPE = instanceFactory[type]; 18 | let instance; 19 | if (TYPE) { 20 | instance = TYPE(rest, paper, children); 21 | if (pathData) { 22 | instance.pathData = pathData; 23 | } 24 | } 25 | return instance; 26 | }; 27 | } 28 | 29 | const createInstance: CreateInstance = getTypes(TYPES); 30 | 31 | type HostContext = {}; 32 | 33 | type Instance = Object; 34 | 35 | type Fiber = {}; 36 | 37 | type PaperScope = typeof Paper.PaperScope; 38 | 39 | /* eslint-disable no-console, no-unused-vars */ 40 | const defaultHostConfig = { 41 | getRootHostContext(paper: PaperScope): PaperScope { 42 | return paper; 43 | }, 44 | getChildHostContext(parentHostContext: HostContext, type: string, instance: Instance): any { 45 | return {}; 46 | }, 47 | getPublicInstance(instance: Instance): Instance { 48 | return instance; 49 | }, 50 | createInstance, 51 | appendInitialChild(parent: Instance, child: Instance) { 52 | if (parent instanceof Paper.Group && child instanceof Paper.Item) { 53 | parent.addChild(child); 54 | } else if (parent instanceof Paper.TextItem && typeof child === 'string') { 55 | Object.assign(parent, { content: child }); 56 | } else { 57 | // console.log('ignore append initial child'); 58 | } 59 | }, 60 | finalizeInitialChildren(instance: Instance, type: string, props: Props): boolean { 61 | return true; 62 | }, 63 | commitMount(instance: Instance, type: string, newProps: Props, internalInstanceHandle: Fiber) { 64 | // console.log('ignore commit mount'); 65 | }, 66 | prepareUpdate( 67 | instance: Instance, 68 | type: string, 69 | oldProps: Props, 70 | newProps: Props, 71 | paper: PaperScope, 72 | hostContext: HostContext, 73 | ): any[] { 74 | return diffProps(oldProps, newProps); 75 | }, 76 | shouldSetTextContent(type: string, props: Props): boolean { 77 | const { children } = props; 78 | return typeof children === 'string'; 79 | }, 80 | shouldDeprioritizeSubtree(type: string, props: Props): boolean { 81 | return false; 82 | }, 83 | createTextInstance( 84 | text: string, 85 | paper: PaperScope, 86 | hostContext: HostContext, 87 | internalInstanceHandle: Fiber, 88 | ): string { 89 | return text; 90 | }, 91 | scheduleDeferredCallback: 92 | (typeof window !== 'undefined' 93 | ? window.requestIdleCallback 94 | : function dummyRequestIdleCallback(callback, options) { 95 | setTimeout(callback, options.timeout); 96 | }: any | ((callback: () => any, options?: any) => void)), 97 | prepareForCommit(): any { 98 | return null; 99 | }, 100 | clearContainer(container: PaperScope) { 101 | // console.log('ignore clear container'); 102 | }, 103 | resetAfterCommit() { 104 | // console.log('ignore reset for commit'); 105 | }, 106 | now: Date.now, 107 | supportsMutation: true, 108 | commitUpdate( 109 | instance: Instance, 110 | updatePayload: [], 111 | type: string, 112 | oldProps: Props, 113 | newProps: Props, 114 | internalInstanceHandle: Fiber, 115 | ) { 116 | updateProps(instance, updatePayload, type, oldProps, newProps); 117 | }, 118 | commitTextUpdate(textInstance: Instance, oldText: string, newText: string) { 119 | // console.log('ignore commit text update'); 120 | }, 121 | resetTextContent(instance: Instance) { 122 | // console.log('ignore reset text content'); 123 | }, 124 | appendChild(parent: Instance, child: Instance) { 125 | if (parent instanceof Paper.Group && child instanceof Paper.Item) { 126 | parent.addChild(child); 127 | } else { 128 | // console.log('ignore append child', parent, child); 129 | } 130 | }, 131 | appendChildToContainer(container: PaperScope, child: Instance) { 132 | if (child instanceof Paper.Item) { 133 | const { project } = container; 134 | const { $$default, $$metadata } = project.layers; 135 | if (child instanceof Paper.Layer) { 136 | child.insertBelow($$metadata); 137 | } else { 138 | child.addTo($$default); 139 | } 140 | } else if (child instanceof Paper.Tool) { 141 | child.activate(); 142 | } else { 143 | // console.log('ignore append child to container', child); 144 | } 145 | }, 146 | insertBefore(parent: Instance, child: Instance, beforeChild: Instance) { 147 | // console.log('ignore insert before child', parent, child, beforeChild); 148 | }, 149 | insertInContainerBefore(container: PaperScope, child: Instance, beforeChild: Instance) { 150 | const { $$default, $$metadata } = container.project.layers; 151 | if (child instanceof Paper.Layer && beforeChild instanceof Paper.Layer) { 152 | child.insertBelow(beforeChild); 153 | } else if (child instanceof Paper.Layer) { 154 | child.insertBelow($$metadata); 155 | } else if (child instanceof Paper.Item && beforeChild instanceof Paper.Layer) { 156 | child.addTo($$default); 157 | } else if (child instanceof Paper.Item && beforeChild instanceof Paper.Item) { 158 | child.insertBelow(beforeChild); 159 | } else { 160 | // console.log('ignore insert in container before child', child, beforeChild); 161 | } 162 | }, 163 | removeChild(parent: Instance, child: Instance) { 164 | child.remove(); 165 | }, 166 | removeChildFromContainer(container: PaperScope, child: Instance) { 167 | if (child instanceof Object) { 168 | child.remove(); 169 | } 170 | }, 171 | }; 172 | /* eslint-enable no-console, no-unused-vars */ 173 | 174 | export default class PaperRenderer { 175 | defaultHostConfig: typeof defaultHostConfig = defaultHostConfig; 176 | 177 | defaultTypes: Types = TYPES; 178 | 179 | reconciler: any; 180 | 181 | createInstance: CreateInstance; 182 | 183 | constructor() { 184 | const instanceFactory = this.getInstanceFactory(); 185 | let hostConfig = this.getHostConfig(); 186 | 187 | /* Overrides createInstance if host config is not changed but types are */ 188 | if (this.defaultTypes !== instanceFactory 189 | && defaultHostConfig === hostConfig) { 190 | this.createInstance = getTypes(instanceFactory); 191 | hostConfig = { 192 | ...hostConfig, 193 | createInstance: this.createInstance, 194 | }; 195 | } else { 196 | this.createInstance = createInstance; 197 | } 198 | 199 | this.reconciler = Reconciler(hostConfig); 200 | } 201 | 202 | getInstanceFactory(): Types { 203 | return this.defaultTypes; 204 | } 205 | 206 | getHostConfig(): typeof defaultHostConfig { 207 | return this.defaultHostConfig; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /packages/react-cache/cjs/react-cache.development.js: -------------------------------------------------------------------------------- 1 | /** @license React v16.6.1 2 | * react-cache.development.js 3 | * 4 | * Copyright (c) Facebook, Inc. and its affiliates. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | 13 | 14 | if (process.env.NODE_ENV !== "production") { 15 | (function() { 16 | 'use strict'; 17 | 18 | Object.defineProperty(exports, '__esModule', { value: true }); 19 | 20 | var React = require('react'); 21 | var scheduler = require('scheduler'); 22 | 23 | /** 24 | * Similar to invariant but only logs a warning if the condition is not met. 25 | * This can be used to log issues in development environments in critical 26 | * paths. Removing the logging code for production environments will keep the 27 | * same logic and follow the same code paths. 28 | */ 29 | 30 | var warningWithoutStack = function () {}; 31 | 32 | { 33 | warningWithoutStack = function (condition, format) { 34 | for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { 35 | args[_key - 2] = arguments[_key]; 36 | } 37 | 38 | if (format === undefined) { 39 | throw new Error('`warningWithoutStack(condition, format, ...args)` requires a warning ' + 'message argument'); 40 | } 41 | if (args.length > 8) { 42 | // Check before the condition to catch violations early. 43 | throw new Error('warningWithoutStack() currently supports at most 8 arguments.'); 44 | } 45 | if (condition) { 46 | return; 47 | } 48 | if (typeof console !== 'undefined') { 49 | var argsWithFormat = args.map(function (item) { 50 | return '' + item; 51 | }); 52 | argsWithFormat.unshift('Warning: ' + format); 53 | 54 | // We intentionally don't use spread (or .apply) directly because it 55 | // breaks IE9: https://github.com/facebook/react/issues/13610 56 | Function.prototype.apply.call(console.error, console, argsWithFormat); 57 | } 58 | try { 59 | // --- Welcome to debugging React --- 60 | // This error was thrown as a convenience so that you can use this stack 61 | // to find the callsite that caused this warning to fire. 62 | var argIndex = 0; 63 | var message = 'Warning: ' + format.replace(/%s/g, function () { 64 | return args[argIndex++]; 65 | }); 66 | throw new Error(message); 67 | } catch (x) {} 68 | }; 69 | } 70 | 71 | var warningWithoutStack$1 = warningWithoutStack; 72 | 73 | function createLRU(limit) { 74 | var LIMIT = limit; 75 | 76 | // Circular, doubly-linked list 77 | var first = null; 78 | var size = 0; 79 | 80 | var cleanUpIsScheduled = false; 81 | 82 | function scheduleCleanUp() { 83 | if (cleanUpIsScheduled === false && size > LIMIT) { 84 | // The cache size exceeds the limit. Schedule a callback to delete the 85 | // least recently used entries. 86 | cleanUpIsScheduled = true; 87 | scheduler.unstable_scheduleCallback(cleanUp); 88 | } 89 | } 90 | 91 | function cleanUp() { 92 | cleanUpIsScheduled = false; 93 | deleteLeastRecentlyUsedEntries(LIMIT); 94 | } 95 | 96 | function deleteLeastRecentlyUsedEntries(targetSize) { 97 | // Delete entries from the cache, starting from the end of the list. 98 | if (first !== null) { 99 | var resolvedFirst = first; 100 | var last = resolvedFirst.previous; 101 | while (size > targetSize && last !== null) { 102 | var _onDelete = last.onDelete; 103 | var _previous = last.previous; 104 | last.onDelete = null; 105 | 106 | // Remove from the list 107 | last.previous = last.next = null; 108 | if (last === first) { 109 | // Reached the head of the list. 110 | first = last = null; 111 | } else { 112 | first.previous = _previous; 113 | _previous.next = first; 114 | last = _previous; 115 | } 116 | 117 | size -= 1; 118 | 119 | // Call the destroy method after removing the entry from the list. If it 120 | // throws, the rest of cache will not be deleted, but it will be in a 121 | // valid state. 122 | _onDelete(); 123 | } 124 | } 125 | } 126 | 127 | function add(value, onDelete) { 128 | var entry = { 129 | value: value, 130 | onDelete: onDelete, 131 | next: null, 132 | previous: null 133 | }; 134 | if (first === null) { 135 | entry.previous = entry.next = entry; 136 | first = entry; 137 | } else { 138 | // Append to head 139 | var last = first.previous; 140 | last.next = entry; 141 | entry.previous = last; 142 | 143 | first.previous = entry; 144 | entry.next = first; 145 | 146 | first = entry; 147 | } 148 | size += 1; 149 | return entry; 150 | } 151 | 152 | function update(entry, newValue) { 153 | entry.value = newValue; 154 | } 155 | 156 | function access(entry) { 157 | var next = entry.next; 158 | if (next !== null) { 159 | // Entry already cached 160 | var resolvedFirst = first; 161 | if (first !== entry) { 162 | // Remove from current position 163 | var _previous2 = entry.previous; 164 | _previous2.next = next; 165 | next.previous = _previous2; 166 | 167 | // Append to head 168 | var last = resolvedFirst.previous; 169 | last.next = entry; 170 | entry.previous = last; 171 | 172 | resolvedFirst.previous = entry; 173 | entry.next = resolvedFirst; 174 | 175 | first = entry; 176 | } 177 | } else { 178 | // Cannot access a deleted entry 179 | // TODO: Error? Warning? 180 | } 181 | scheduleCleanUp(); 182 | return entry.value; 183 | } 184 | 185 | function setLimit(newLimit) { 186 | LIMIT = newLimit; 187 | scheduleCleanUp(); 188 | } 189 | 190 | return { 191 | add: add, 192 | update: update, 193 | access: access, 194 | setLimit: setLimit 195 | }; 196 | } 197 | 198 | var Pending = 0; 199 | var Resolved = 1; 200 | var Rejected = 2; 201 | 202 | var ReactCurrentDispatcher = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher; 203 | 204 | function readContext(Context, observedBits) { 205 | var dispatcher = ReactCurrentDispatcher.current; 206 | if (dispatcher === null) { 207 | throw new Error('react-cache: read and preload may only be called from within a ' + "component's render. They are not supported in event handlers or " + 'lifecycle methods.'); 208 | } 209 | return dispatcher.readContext(Context, observedBits); 210 | } 211 | 212 | function identityHashFn(input) { 213 | { 214 | !(typeof input === 'string' || typeof input === 'number' || typeof input === 'boolean' || input === undefined || input === null) ? warningWithoutStack$1(false, 'Invalid key type. Expected a string, number, symbol, or boolean, ' + 'but instead received: %s' + '\n\nTo use non-primitive values as keys, you must pass a hash ' + 'function as the second argument to createResource().', input) : void 0; 215 | } 216 | return input; 217 | } 218 | 219 | var CACHE_LIMIT = 500; 220 | var lru = createLRU(CACHE_LIMIT); 221 | 222 | var entries = new Map(); 223 | 224 | var CacheContext = React.createContext(null); 225 | 226 | function accessResult(resource, fetch, input, key) { 227 | var entriesForResource = entries.get(resource); 228 | if (entriesForResource === undefined) { 229 | entriesForResource = new Map(); 230 | entries.set(resource, entriesForResource); 231 | } 232 | var entry = entriesForResource.get(key); 233 | if (entry === undefined) { 234 | var thenable = fetch(input); 235 | thenable.then(function (value) { 236 | if (newResult.status === Pending) { 237 | var resolvedResult = newResult; 238 | resolvedResult.status = Resolved; 239 | resolvedResult.value = value; 240 | } 241 | }, function (error) { 242 | if (newResult.status === Pending) { 243 | var rejectedResult = newResult; 244 | rejectedResult.status = Rejected; 245 | rejectedResult.value = error; 246 | } 247 | }); 248 | var newResult = { 249 | status: Pending, 250 | value: thenable 251 | }; 252 | var newEntry = lru.add(newResult, deleteEntry.bind(null, resource, key)); 253 | entriesForResource.set(key, newEntry); 254 | return newResult; 255 | } else { 256 | return lru.access(entry); 257 | } 258 | } 259 | 260 | function deleteEntry(resource, key) { 261 | var entriesForResource = entries.get(resource); 262 | if (entriesForResource !== undefined) { 263 | entriesForResource.delete(key); 264 | if (entriesForResource.size === 0) { 265 | entries.delete(resource); 266 | } 267 | } 268 | } 269 | 270 | function unstable_createResource(fetch, maybeHashInput) { 271 | var hashInput = maybeHashInput !== undefined ? maybeHashInput : identityHashFn; 272 | 273 | var resource = { 274 | read: function (input) { 275 | // react-cache currently doesn't rely on context, but it may in the 276 | // future, so we read anyway to prevent access outside of render. 277 | readContext(CacheContext); 278 | var key = hashInput(input); 279 | var result = accessResult(resource, fetch, input, key); 280 | switch (result.status) { 281 | case Pending: 282 | { 283 | var suspender = result.value; 284 | throw suspender; 285 | } 286 | case Resolved: 287 | { 288 | var _value = result.value; 289 | return _value; 290 | } 291 | case Rejected: 292 | { 293 | var error = result.value; 294 | throw error; 295 | } 296 | default: 297 | // Should be unreachable 298 | return undefined; 299 | } 300 | }, 301 | preload: function (input) { 302 | // react-cache currently doesn't rely on context, but it may in the 303 | // future, so we read anyway to prevent access outside of render. 304 | readContext(CacheContext); 305 | var key = hashInput(input); 306 | accessResult(resource, fetch, input, key); 307 | } 308 | }; 309 | return resource; 310 | } 311 | 312 | function unstable_setGlobalCacheLimit(limit) { 313 | lru.setLimit(limit); 314 | } 315 | 316 | exports.unstable_createResource = unstable_createResource; 317 | exports.unstable_setGlobalCacheLimit = unstable_setGlobalCacheLimit; 318 | })(); 319 | } 320 | -------------------------------------------------------------------------------- /packages/react-cache/umd/react-cache.development.js: -------------------------------------------------------------------------------- 1 | /** @license React v16.6.1 2 | * react-cache.development.js 3 | * 4 | * Copyright (c) Facebook, Inc. and its affiliates. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | (function (global, factory) { 13 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('scheduler')) : 14 | typeof define === 'function' && define.amd ? define(['exports', 'react', 'scheduler'], factory) : 15 | (factory((global.ReactCache = {}),global.React,global.Scheduler)); 16 | }(this, (function (exports,React,scheduler) { 'use strict'; 17 | 18 | /** 19 | * Similar to invariant but only logs a warning if the condition is not met. 20 | * This can be used to log issues in development environments in critical 21 | * paths. Removing the logging code for production environments will keep the 22 | * same logic and follow the same code paths. 23 | */ 24 | 25 | var warningWithoutStack = function () {}; 26 | 27 | { 28 | warningWithoutStack = function (condition, format) { 29 | for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { 30 | args[_key - 2] = arguments[_key]; 31 | } 32 | 33 | if (format === undefined) { 34 | throw new Error('`warningWithoutStack(condition, format, ...args)` requires a warning ' + 'message argument'); 35 | } 36 | if (args.length > 8) { 37 | // Check before the condition to catch violations early. 38 | throw new Error('warningWithoutStack() currently supports at most 8 arguments.'); 39 | } 40 | if (condition) { 41 | return; 42 | } 43 | if (typeof console !== 'undefined') { 44 | var argsWithFormat = args.map(function (item) { 45 | return '' + item; 46 | }); 47 | argsWithFormat.unshift('Warning: ' + format); 48 | 49 | // We intentionally don't use spread (or .apply) directly because it 50 | // breaks IE9: https://github.com/facebook/react/issues/13610 51 | Function.prototype.apply.call(console.error, console, argsWithFormat); 52 | } 53 | try { 54 | // --- Welcome to debugging React --- 55 | // This error was thrown as a convenience so that you can use this stack 56 | // to find the callsite that caused this warning to fire. 57 | var argIndex = 0; 58 | var message = 'Warning: ' + format.replace(/%s/g, function () { 59 | return args[argIndex++]; 60 | }); 61 | throw new Error(message); 62 | } catch (x) {} 63 | }; 64 | } 65 | 66 | var warningWithoutStack$1 = warningWithoutStack; 67 | 68 | function createLRU(limit) { 69 | var LIMIT = limit; 70 | 71 | // Circular, doubly-linked list 72 | var first = null; 73 | var size = 0; 74 | 75 | var cleanUpIsScheduled = false; 76 | 77 | function scheduleCleanUp() { 78 | if (cleanUpIsScheduled === false && size > LIMIT) { 79 | // The cache size exceeds the limit. Schedule a callback to delete the 80 | // least recently used entries. 81 | cleanUpIsScheduled = true; 82 | scheduler.unstable_scheduleCallback(cleanUp); 83 | } 84 | } 85 | 86 | function cleanUp() { 87 | cleanUpIsScheduled = false; 88 | deleteLeastRecentlyUsedEntries(LIMIT); 89 | } 90 | 91 | function deleteLeastRecentlyUsedEntries(targetSize) { 92 | // Delete entries from the cache, starting from the end of the list. 93 | if (first !== null) { 94 | var resolvedFirst = first; 95 | var last = resolvedFirst.previous; 96 | while (size > targetSize && last !== null) { 97 | var _onDelete = last.onDelete; 98 | var _previous = last.previous; 99 | last.onDelete = null; 100 | 101 | // Remove from the list 102 | last.previous = last.next = null; 103 | if (last === first) { 104 | // Reached the head of the list. 105 | first = last = null; 106 | } else { 107 | first.previous = _previous; 108 | _previous.next = first; 109 | last = _previous; 110 | } 111 | 112 | size -= 1; 113 | 114 | // Call the destroy method after removing the entry from the list. If it 115 | // throws, the rest of cache will not be deleted, but it will be in a 116 | // valid state. 117 | _onDelete(); 118 | } 119 | } 120 | } 121 | 122 | function add(value, onDelete) { 123 | var entry = { 124 | value: value, 125 | onDelete: onDelete, 126 | next: null, 127 | previous: null 128 | }; 129 | if (first === null) { 130 | entry.previous = entry.next = entry; 131 | first = entry; 132 | } else { 133 | // Append to head 134 | var last = first.previous; 135 | last.next = entry; 136 | entry.previous = last; 137 | 138 | first.previous = entry; 139 | entry.next = first; 140 | 141 | first = entry; 142 | } 143 | size += 1; 144 | return entry; 145 | } 146 | 147 | function update(entry, newValue) { 148 | entry.value = newValue; 149 | } 150 | 151 | function access(entry) { 152 | var next = entry.next; 153 | if (next !== null) { 154 | // Entry already cached 155 | var resolvedFirst = first; 156 | if (first !== entry) { 157 | // Remove from current position 158 | var _previous2 = entry.previous; 159 | _previous2.next = next; 160 | next.previous = _previous2; 161 | 162 | // Append to head 163 | var last = resolvedFirst.previous; 164 | last.next = entry; 165 | entry.previous = last; 166 | 167 | resolvedFirst.previous = entry; 168 | entry.next = resolvedFirst; 169 | 170 | first = entry; 171 | } 172 | } else { 173 | // Cannot access a deleted entry 174 | // TODO: Error? Warning? 175 | } 176 | scheduleCleanUp(); 177 | return entry.value; 178 | } 179 | 180 | function setLimit(newLimit) { 181 | LIMIT = newLimit; 182 | scheduleCleanUp(); 183 | } 184 | 185 | return { 186 | add: add, 187 | update: update, 188 | access: access, 189 | setLimit: setLimit 190 | }; 191 | } 192 | 193 | var Pending = 0; 194 | var Resolved = 1; 195 | var Rejected = 2; 196 | 197 | var ReactCurrentDispatcher = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher; 198 | 199 | function readContext(Context, observedBits) { 200 | var dispatcher = ReactCurrentDispatcher.current; 201 | if (dispatcher === null) { 202 | throw new Error('react-cache: read and preload may only be called from within a ' + "component's render. They are not supported in event handlers or " + 'lifecycle methods.'); 203 | } 204 | return dispatcher.readContext(Context, observedBits); 205 | } 206 | 207 | function identityHashFn(input) { 208 | { 209 | !(typeof input === 'string' || typeof input === 'number' || typeof input === 'boolean' || input === undefined || input === null) ? warningWithoutStack$1(false, 'Invalid key type. Expected a string, number, symbol, or boolean, ' + 'but instead received: %s' + '\n\nTo use non-primitive values as keys, you must pass a hash ' + 'function as the second argument to createResource().', input) : void 0; 210 | } 211 | return input; 212 | } 213 | 214 | var CACHE_LIMIT = 500; 215 | var lru = createLRU(CACHE_LIMIT); 216 | 217 | var entries = new Map(); 218 | 219 | var CacheContext = React.createContext(null); 220 | 221 | function accessResult(resource, fetch, input, key) { 222 | var entriesForResource = entries.get(resource); 223 | if (entriesForResource === undefined) { 224 | entriesForResource = new Map(); 225 | entries.set(resource, entriesForResource); 226 | } 227 | var entry = entriesForResource.get(key); 228 | if (entry === undefined) { 229 | var thenable = fetch(input); 230 | thenable.then(function (value) { 231 | if (newResult.status === Pending) { 232 | var resolvedResult = newResult; 233 | resolvedResult.status = Resolved; 234 | resolvedResult.value = value; 235 | } 236 | }, function (error) { 237 | if (newResult.status === Pending) { 238 | var rejectedResult = newResult; 239 | rejectedResult.status = Rejected; 240 | rejectedResult.value = error; 241 | } 242 | }); 243 | var newResult = { 244 | status: Pending, 245 | value: thenable 246 | }; 247 | var newEntry = lru.add(newResult, deleteEntry.bind(null, resource, key)); 248 | entriesForResource.set(key, newEntry); 249 | return newResult; 250 | } else { 251 | return lru.access(entry); 252 | } 253 | } 254 | 255 | function deleteEntry(resource, key) { 256 | var entriesForResource = entries.get(resource); 257 | if (entriesForResource !== undefined) { 258 | entriesForResource.delete(key); 259 | if (entriesForResource.size === 0) { 260 | entries.delete(resource); 261 | } 262 | } 263 | } 264 | 265 | function unstable_createResource(fetch, maybeHashInput) { 266 | var hashInput = maybeHashInput !== undefined ? maybeHashInput : identityHashFn; 267 | 268 | var resource = { 269 | read: function (input) { 270 | // react-cache currently doesn't rely on context, but it may in the 271 | // future, so we read anyway to prevent access outside of render. 272 | readContext(CacheContext); 273 | var key = hashInput(input); 274 | var result = accessResult(resource, fetch, input, key); 275 | switch (result.status) { 276 | case Pending: 277 | { 278 | var suspender = result.value; 279 | throw suspender; 280 | } 281 | case Resolved: 282 | { 283 | var _value = result.value; 284 | return _value; 285 | } 286 | case Rejected: 287 | { 288 | var error = result.value; 289 | throw error; 290 | } 291 | default: 292 | // Should be unreachable 293 | return undefined; 294 | } 295 | }, 296 | preload: function (input) { 297 | // react-cache currently doesn't rely on context, but it may in the 298 | // future, so we read anyway to prevent access outside of render. 299 | readContext(CacheContext); 300 | var key = hashInput(input); 301 | accessResult(resource, fetch, input, key); 302 | } 303 | }; 304 | return resource; 305 | } 306 | 307 | function unstable_setGlobalCacheLimit(limit) { 308 | lru.setLimit(limit); 309 | } 310 | 311 | exports.unstable_createResource = unstable_createResource; 312 | exports.unstable_setGlobalCacheLimit = unstable_setGlobalCacheLimit; 313 | 314 | Object.defineProperty(exports, '__esModule', { value: true }); 315 | 316 | }))); 317 | --------------------------------------------------------------------------------