Skip to content

Commit

Permalink
Canvas
Browse files Browse the repository at this point in the history
  • Loading branch information
Antonio Russo committed Nov 22, 2020
1 parent 5deecd8 commit 5d66b0f
Show file tree
Hide file tree
Showing 40 changed files with 353 additions and 61 deletions.
6 changes: 0 additions & 6 deletions .babelrc

This file was deleted.

7 changes: 6 additions & 1 deletion docs/setup/styleguidist.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@ module.exports = {
content: '../getting-started.md',
sectionDepth: 1,
},
{
name: 'Canvas',
content: '../../src/components/Canvas/README.md',
sectionDepth: 1,
},
{
name: 'Diagram Component',
content: '../../src/Diagram/README.md',
content: '../../src/components/Diagram/README.md',
sectionDepth: 1,
},
{
Expand Down
30 changes: 9 additions & 21 deletions docs/setup/styleguidist.webpack.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,35 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const sourcePath = path.resolve(__dirname, '../..', 'src');

module.exports = () => ({
entry: [
`${sourcePath}/theme/index.scss`,
`${sourcePath}/index.js`,
],
devtool: 'inline-source-map',
output: {
filename: 'beautiful-react-diagrams.dev.js',
},
resolve: {
extensions: ['.js', '.jsx', 'scss'],
alias: { 'beautiful-react-diagrams': sourcePath },
},
devServer: {
contentBase: sourcePath,
open: true,
hot: false,
liveReload: true,
watchContentBase: true,
hot: true,
},
mode: 'development',
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react'
]
}
},
},
{
test: /\.(css|scss)$/,
exclude: /node_modules/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
{ loader: 'sass-loader' },
],
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
},
{
test: /\.png$/,
Expand Down
21 changes: 10 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"scripts": {
"build": "npx del-cli dist && rollup -c",
"build-doc": "npx styleguidist build --config docs/setup/styleguidist.config.js",
"start": "styleguidist server --config docs/setup/styleguidist.config.js",
"start": "npx styleguidist server --config docs/setup/styleguidist.config.js",
"lint-js": "eslint --ext .jsx,.js src/",
"lint-tests": "eslint --ext .jsx,.js tests/",
"lint-scss": "stylelint '**/*.scss'",
Expand All @@ -31,18 +31,18 @@
},
"devDependencies": {
"@babel/cli": "^7.12.1",
"@babel/core": "^7.12.3",
"@babel/core": "7.12.7",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.12.1",
"@babel/preset-env": "7.12.7",
"@babel/preset-react": "^7.12.1",
"@babel/register": "^7.12.1",
"@rollup/plugin-babel": "^5.2.1",
"@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-node-resolve": "9.0.0",
"@testing-library/react": "^11.1.0",
"@types/react": "^16.9.53",
"autoprefixer": "9.0.0",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"babel-loader": "8.2.1",
"beautiful-react-ui": "^0.56.14",
"chai": "^4.2.0",
"css-loader": "^5.0.0",
Expand All @@ -61,17 +61,17 @@
"jsdom-global": "^3.0.2",
"mini-css-extract-plugin": "^1.1.1",
"mocha": "^8.2.0",
"node-sass": "^4.14.1",
"node-sass": "4.14.1",
"nyc": "^15.1.0",
"postcss": "^8.1.2",
"postcss-fixes": "^2.0.1",
"postcss-normalize": "^9.0.0",
"postcss-preset-env": "^6.7.0",
"postcss-will-change": "3.0.0",
"postcss-will-change-transition": "^1.2.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-styleguidist": "^11.1.0",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-styleguidist": "11.1.3",
"rollup": "^2.32.1",
"rollup-plugin-postcss": "^3.1.8",
"sass": "^1.27.0",
Expand All @@ -84,8 +84,7 @@
"stylelint-scss": "^3.18.0",
"typescript": "4.0.5",
"url-loader": "^4.1.1",
"webpack": "4.44.0",
"webpack-cli": "4.1.0"
"webpack": "5.6.0"
},
"dependencies": {
"beautiful-react-hooks": "^0.31.0",
Expand Down
49 changes: 49 additions & 0 deletions src/components/Canvas/BackgroundGrid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';

const parallaxRatio = 1.25;
const calcCoordinates = (x, y) => ([x * parallaxRatio, y * parallaxRatio]);
const calcTransformation = (x, y, scale) => (`scale(${scale}) translate(${x}, ${y})`);

/**
* Canvas background
*/
const BackgroundGrid = ({ translateX, translateY, scale, svgPatternColor, svgPatternOpacity }) => {
const [x, y] = useMemo(() => calcCoordinates(translateX, translateY), [translateX, translateY]);
const transformation = useMemo(() => calcTransformation(x, y, scale), [x, y, scale]);

return (
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="bg-grid" width="30" height="30" patternUnits="userSpaceOnUse" patternTransform={transformation}>
<g opacity={svgPatternOpacity} fill={svgPatternColor}>
<polygon points="29.6,0 27,0 27,0.4 29.6,0.4 29.6,3 30,3 30,0.4 30,0 " />
<polygon points="0,0 0,0.4 0,3 0.4,3 0.4,0.4 3,0.4 3,0 0.4,0 " />
<polygon points="30,30 30,29.6 30,27 29.6,27 29.6,29.6 27,29.6 27,30 29.6,30 " />
<polygon points="0.4,30 3,30 3,29.6 0.4,29.6 0.4,27 0,27 0,29.6 0,30 " />
</g>
</pattern>

</defs>
<rect width="100%" height="100%" fill="url(#bg-grid)" />
</svg>
);
};

BackgroundGrid.propTypes = {
svgPatternColor: PropTypes.string,
svgPatternOpacity: PropTypes.number,
translateX: PropTypes.number,
translateY: PropTypes.number,
scale: PropTypes.number,
};

BackgroundGrid.defaultProps = {
svgPatternColor: 'black',
svgPatternOpacity: 0.5,
translateX: 0,
translateY: 0,
scale: 1,
};

export default React.memo(BackgroundGrid);
70 changes: 70 additions & 0 deletions src/components/Canvas/Canvas.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import useCanvasPan from './useCanvasPan';
import useCanvasZoom from './useCanvasZoom';
import BackgroundGrid from './BackgroundGrid';

import './canvas.scss';

const makeStyle = (scale = 1, { x = 0, y = 0 }) => ({
transform: `translate(${x}px, ${y}px) translateZ(0) scale(${scale})`,
});

/**
* @TODO: Document this component
*/
const Canvas = (props) => {
const {
initialZoom, maxZoom, minZoom, zoomable, pannable, zoomOnWheel, inertia, debug, children, className,
ElementRenderer, GridRenderer, ...rest
} = props;
const elRef = useRef();
const [pan, startPan] = useCanvasPan({ pannable, inertia });
const [scale] = useCanvasZoom(elRef, { initialZoom, maxZoom, minZoom, zoomable, zoomOnWheel });
const classList = useMemo(() => classNames('bi bi-diagram bi-diagram-canvas', className), [className]);
const style = useMemo(() => makeStyle(scale, pan), [scale, pan.x, pan.y]);

return (
<ElementRenderer className={classList} onMouseDown={startPan} onTouchStart={startPan} ref={elRef} {...rest}>
<GridRenderer translateX={pan.x} translateY={pan.y} scale={scale} />
<div className="bi-canvas-content" style={style}>
{children}
</div>
{debug && (
<div className="bi-canvas-debugger">
<p>{JSON.stringify(pan)}</p>
<p>{`Scale: ${scale}`}</p>
</div>
)}
</ElementRenderer>
);
};

Canvas.propTypes = {
zoomable: PropTypes.bool,
pannable: PropTypes.bool,
initialZoom: PropTypes.number,
zoomOnWheel: PropTypes.bool,
maxZoom: PropTypes.number,
minZoom: PropTypes.number,
inertia: PropTypes.bool,
debug: PropTypes.bool,
GridRenderer: PropTypes.elementType,
ElementRenderer: PropTypes.elementType,
};

Canvas.defaultProps = {
zoomable: true,
pannable: true,
initialZoom: 1,
zoomOnWheel: true,
maxZoom: 5,
minZoom: 0.4,
inertia: true,
debug: false,
GridRenderer: BackgroundGrid,
ElementRenderer: 'div',
};

export default React.memo(Canvas);
9 changes: 9 additions & 0 deletions src/components/Canvas/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
```
import { Canvas } from 'beautiful-react-diagrams';
<div style={{ height: '30rem' }}>
<Canvas debug>
<p>Element</p>
</Canvas>
</div>
```
39 changes: 39 additions & 0 deletions src/components/Canvas/canvas.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.bi.bi-diagram.bi-diagram-canvas {
position: relative;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
background: #fbfaf9;
border: 0.07rem solid rgba(0, 0, 0, 0.2);
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;

&:active {
cursor: grabbing;
}

.bi-canvas-content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}

.bi-canvas-debugger {
max-width: 24rem;
height: 3rem;
position: absolute;
bottom: 0;
right: 0;
background: rgba(0, 0, 0, 0.1);

p {
margin: 0;
}
}
}
1 change: 1 addition & 0 deletions src/components/Canvas/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './Canvas';
76 changes: 76 additions & 0 deletions src/components/Canvas/useCanvasPan.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useState, useCallback, useRef } from 'react';
import Events from '../../shared/Events';
import { isTouch } from '../../shared/Constants';

const initialState = { x: 0, y: 0 };
const friction = 0.8; // TODO: document this stuff
const getMouseEventPoint = (e) => ({ x: e.pageX, y: e.pageY });
const getTouchEventPoint = (e) => getMouseEventPoint(e.changedTouches[0]);
const getEventPoint = isTouch ? getTouchEventPoint : getMouseEventPoint;
const getDelta = (point, lastPoint) => ({ x: lastPoint.x - point.x, y: lastPoint.y - point.y });
const applyInertia = (value) => (Math.abs(value) >= 0.5 ? Math.trunc(value * friction) : 0);

/**
* TODO: document this thing
* Inspired by this article:
* https://jclem.net/posts/pan-zoom-canvas-react?utm_campaign=building-a-pannable--zoomable-canvasdi
*/
const useCanvasPan = ({ pannable, inertia }) => {
const [pan, setPan] = useState(initialState);
const lastPointRef = useRef(initialState);
const deltaRef = useRef({ x: null, y: null });

// TODO: document this callback
const performPan = useCallback((event) => {
if (pannable) {
const lastPoint = { ...lastPointRef.current };
const point = getEventPoint(event);
lastPointRef.current = point;
setPan(({ x, y }) => {
const delta = getDelta(lastPoint, point);
deltaRef.current = { ...delta };

return { x: x + delta.x, y: y + delta.y };
});
}
}, []);

// TODO: document this callback
const performInertia = useCallback(() => {
if (inertia) {
setPan(({ x, y }) => ({ x: x + deltaRef.current.x, y: y + deltaRef.current.y }));

deltaRef.current.x = applyInertia(deltaRef.current.x);
deltaRef.current.y = applyInertia(deltaRef.current.y);

if (Math.abs(deltaRef.current.x) > 0 || Math.abs(deltaRef.current.y) > 0) {
requestAnimationFrame(performInertia);
}
}
}, [inertia, deltaRef.current.x, deltaRef.current.y]);

// TODO: document this callback
const endPan = useCallback(() => {
if (pannable) {
document.removeEventListener(Events.MOUSE_MOVE, performPan);
document.removeEventListener(Events.MOUSE_END, endPan);

if (inertia) {
requestAnimationFrame(performInertia);
}
}
}, [performPan]);

// TODO: document this callback
const onPanStart = useCallback((event) => {
if (pannable) {
document.addEventListener(Events.MOUSE_MOVE, performPan);
document.addEventListener(Events.MOUSE_END, endPan);
lastPointRef.current = getEventPoint(event);
}
}, [performPan, endPan]);

return [pan, onPanStart];
};

export default useCanvasPan;
Loading

0 comments on commit 5d66b0f

Please sign in to comment.