Scrollbars (#602)

This commit is contained in:
DD Liu 2018-08-16 16:49:43 -04:00 committed by GitHub
parent 2791866a9e
commit 50de05cee4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 297 additions and 56 deletions

View file

@ -90,10 +90,6 @@ $border-radius: 0.25rem;
overflow: visible; overflow: visible;
} }
.with-eye-dropper {
cursor: none;
}
.mode-selector { .mode-selector {
display: flex; display: flex;
margin-right: calc(2 * $grid-unit); margin-right: calc(2 * $grid-unit);

View file

@ -5,6 +5,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import PaperCanvas from '../../containers/paper-canvas.jsx'; import PaperCanvas from '../../containers/paper-canvas.jsx';
import ScrollableCanvas from '../../containers/scrollable-canvas.jsx';
import BitBrushMode from '../../containers/bit-brush-mode.jsx'; import BitBrushMode from '../../containers/bit-brush-mode.jsx';
import BitLineMode from '../../containers/bit-line-mode.jsx'; import BitLineMode from '../../containers/bit-line-mode.jsx';
@ -200,11 +201,10 @@ const PaintEditorComponent = props => (
<div> <div>
{/* Canvas */} {/* Canvas */}
<div <ScrollableCanvas
className={classNames( canvas={props.canvas}
styles.canvasContainer, hideCursor={props.isEyeDropping}
{[styles.withEyeDropper]: props.isEyeDropping} style={styles.canvasContainer}
)}
> >
<PaperCanvas <PaperCanvas
canvasRef={props.setCanvas} canvasRef={props.setCanvas}
@ -231,7 +231,7 @@ const PaintEditorComponent = props => (
</Box> </Box>
) : null ) : null
} }
</div> </ScrollableCanvas>
<div className={styles.canvasControls}> <div className={styles.canvasControls}>
{isVector(props.format) ? {isVector(props.format) ?
<Button <Button

View file

@ -0,0 +1,36 @@
$scrollbar-size: 6px;
$scrollbar-padding: 1px;
.vertical-scrollbar, .horizontal-scrollbar {
position: absolute;
background: #BEBEBECD;
border-radius: 3px;
cursor: pointer;
}
.vertical-scrollbar-wrapper {
position: absolute;
width: $scrollbar-size;
right: $scrollbar-padding;
top: $scrollbar-padding;
height: calc(100% - $scrollbar-size - $scrollbar-padding);
}
.horizontal-scrollbar-wrapper {
position: absolute;
height: $scrollbar-size;
left: $scrollbar-padding;
bottom: $scrollbar-padding;
width: calc(100% - $scrollbar-size - $scrollbar-padding);
}
.vertical-scrollbar {
width: $scrollbar-size;
}
.horizontal-scrollbar {
height: $scrollbar-size;
}
.hide-cursor {
cursor: none;
}

View file

@ -0,0 +1,54 @@
import classNames from 'classnames';
import React from 'react';
import PropTypes from 'prop-types';
import styles from './scrollable-canvas.css';
const ScrollableCanvasComponent = props => (
<div
className={classNames(
props.style,
{[styles.hideCursor]: props.hideCursor}
)}
>
{props.children}
<div className={styles.horizontalScrollbarWrapper}>
<div
className={styles.horizontalScrollbar}
style={{
width: `${props.horizontalScrollLengthPercent}%`,
left: `${props.horizontalScrollStartPercent}%`,
visibility: `${props.hideCursor ||
Math.abs(props.horizontalScrollLengthPercent - 100) < 1e-8 ? 'hidden' : 'visible'}`
}}
onMouseDown={props.onHorizontalScrollbarMouseDown}
/>
</div>
<div className={styles.verticalScrollbarWrapper}>
<div
className={styles.verticalScrollbar}
style={{
height: `${props.verticalScrollLengthPercent}%`,
top: `${props.verticalScrollStartPercent}%`,
visibility: `${props.hideCursor ||
Math.abs(props.verticalScrollLengthPercent - 100) < 1e-8 ? 'hidden' : 'visible'}`
}}
onMouseDown={props.onVerticalScrollbarMouseDown}
/>
</div>
</div>
);
ScrollableCanvasComponent.propTypes = {
children: PropTypes.node.isRequired,
hideCursor: PropTypes.bool,
horizontalScrollLengthPercent: PropTypes.number,
horizontalScrollStartPercent: PropTypes.number,
onHorizontalScrollbarMouseDown: PropTypes.func.isRequired,
onVerticalScrollbarMouseDown: PropTypes.func.isRequired,
style: PropTypes.string,
verticalScrollLengthPercent: PropTypes.number,
verticalScrollStartPercent: PropTypes.number
};
export default ScrollableCanvasComponent;

View file

@ -100,8 +100,11 @@ class PaintEditor extends React.Component {
this.startEyeDroppingLoop(); this.startEyeDroppingLoop();
} else if (!this.props.isEyeDropping && prevProps.isEyeDropping) { } else if (!this.props.isEyeDropping && prevProps.isEyeDropping) {
this.stopEyeDroppingLoop(); this.stopEyeDroppingLoop();
} else if (this.props.isEyeDropping && this.props.viewBounds !== prevProps.viewBounds) {
this.props.previousTool.activate();
this.props.onDeactivateEyeDropper();
this.stopEyeDroppingLoop();
} }
if (this.props.format === Formats.VECTOR && isBitmap(prevProps.format)) { if (this.props.format === Formats.VECTOR && isBitmap(prevProps.format)) {
this.isSwitchingFormats = false; this.isSwitchingFormats = false;
convertToVector(this.props.clearSelectedItems, this.handleUpdateImage); convertToVector(this.props.clearSelectedItems, this.handleUpdateImage);
@ -329,7 +332,6 @@ class PaintEditor extends React.Component {
this.props.previousTool.activate(); this.props.previousTool.activate();
this.props.onDeactivateEyeDropper(); this.props.onDeactivateEyeDropper();
this.stopEyeDroppingLoop(); this.stopEyeDroppingLoop();
this.setState({colorInfo: null});
} }
} }
startEyeDroppingLoop () { startEyeDroppingLoop () {
@ -367,6 +369,7 @@ class PaintEditor extends React.Component {
} }
stopEyeDroppingLoop () { stopEyeDroppingLoop () {
clearInterval(this.intervalId); clearInterval(this.intervalId);
this.setState({colorInfo: null});
} }
render () { render () {
return ( return (
@ -442,7 +445,8 @@ PaintEditor.propTypes = {
stack: PropTypes.arrayOf(PropTypes.object).isRequired, stack: PropTypes.arrayOf(PropTypes.object).isRequired,
pointer: PropTypes.number.isRequired pointer: PropTypes.number.isRequired
}), }),
updateViewBounds: PropTypes.func.isRequired updateViewBounds: PropTypes.func.isRequired,
viewBounds: PropTypes.instanceOf(paper.Matrix).isRequired
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => ({
@ -455,7 +459,8 @@ const mapStateToProps = state => ({
previousTool: state.scratchPaint.color.eyeDropper.previousTool, previousTool: state.scratchPaint.color.eyeDropper.previousTool,
selectedItems: state.scratchPaint.selectedItems, selectedItems: state.scratchPaint.selectedItems,
textEditing: state.scratchPaint.textEditTarget !== null, textEditing: state.scratchPaint.textEditTarget !== null,
undoState: state.scratchPaint.undo undoState: state.scratchPaint.undo,
viewBounds: state.scratchPaint.viewBounds
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
onKeyPress: event => { onKeyPress: event => {

View file

@ -1,7 +1,7 @@
.paper-canvas { .paper-canvas {
width: 480px; width: 480px;
height: 360px; height: 360px;
margin: auto; margin: auto;
position: absolute; position: absolute;
background-color: #fff; background-color: #fff;
} }

View file

@ -14,13 +14,12 @@ import {isGroup, ungroupItems} from '../helper/group';
import {clearRaster, getRaster, setupLayers} from '../helper/layer'; import {clearRaster, getRaster, setupLayers} from '../helper/layer';
import {deleteSelection, getSelectedLeafItems} from '../helper/selection'; import {deleteSelection, getSelectedLeafItems} from '../helper/selection';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, pan, resetZoom, zoomOnFixedPoint} from '../helper/view'; import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, resetZoom} from '../helper/view';
import {ensureClockwise, scaleWithStrokes} from '../helper/math'; import {ensureClockwise, scaleWithStrokes} from '../helper/math';
import {clearHoveredItem} from '../reducers/hover'; import {clearHoveredItem} from '../reducers/hover';
import {clearPasteOffset} from '../reducers/clipboard'; import {clearPasteOffset} from '../reducers/clipboard';
import {updateViewBounds} from '../reducers/view-bounds';
import {changeFormat} from '../reducers/format'; import {changeFormat} from '../reducers/format';
import {updateViewBounds} from '../reducers/view-bounds';
import styles from './paper-canvas.css'; import styles from './paper-canvas.css';
class PaperCanvas extends React.Component { class PaperCanvas extends React.Component {
@ -30,7 +29,6 @@ class PaperCanvas extends React.Component {
'setCanvas', 'setCanvas',
'importSvg', 'importSvg',
'handleKeyDown', 'handleKeyDown',
'handleWheel',
'switchCostume' 'switchCostume'
]); ]);
} }
@ -214,38 +212,6 @@ class PaperCanvas extends React.Component {
this.props.canvasRef(canvas); this.props.canvasRef(canvas);
} }
} }
handleWheel (event) {
// Multiplier variable, so that non-pixel-deltaModes are supported. Needed for Firefox.
// See #529 (or LLK/scratch-blocks#1190).
const multiplier = event.deltaMode === 0x1 ? 15 : 1;
const deltaX = event.deltaX * multiplier;
const deltaY = event.deltaY * multiplier;
if (event.metaKey || event.ctrlKey) {
// Zoom keeping mouse location fixed
const canvasRect = this.canvas.getBoundingClientRect();
const offsetX = event.clientX - canvasRect.left;
const offsetY = event.clientY - canvasRect.top;
const fixedPoint = paper.project.view.viewToProject(
new paper.Point(offsetX, offsetY)
);
zoomOnFixedPoint(-deltaY / 100, fixedPoint);
this.props.updateViewBounds(paper.view.matrix);
this.props.setSelectedItems(this.props.format);
} else if (event.shiftKey && event.deltaX === 0) {
// Scroll horizontally (based on vertical scroll delta)
// This is needed as for some browser/system combinations which do not set deltaX.
// See #156.
const dx = deltaY / paper.project.view.zoom;
pan(dx, 0);
this.props.updateViewBounds(paper.view.matrix);
} else {
const dx = deltaX / paper.project.view.zoom;
const dy = deltaY / paper.project.view.zoom;
pan(dx, dy);
this.props.updateViewBounds(paper.view.matrix);
}
event.preventDefault();
}
render () { render () {
return ( return (
<canvas <canvas
@ -253,7 +219,6 @@ class PaperCanvas extends React.Component {
height="360px" height="360px"
ref={this.setCanvas} ref={this.setCanvas}
width="480px" width="480px"
onWheel={this.handleWheel}
/> />
); );
} }

View file

@ -0,0 +1,174 @@
import paper from '@scratch/paper';
import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
import ScrollableCanvasComponent from '../components/scrollable-canvas/scrollable-canvas.jsx';
import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, clampViewBounds, pan, zoomOnFixedPoint} from '../helper/view';
import {updateViewBounds} from '../reducers/view-bounds';
import {redrawSelectionBox} from '../reducers/selected-items';
import {getEventXY} from '../lib/touch-utils';
import bindAll from 'lodash.bindall';
class ScrollableCanvas extends React.Component {
static get ZOOM_INCREMENT () {
return 0.5;
}
constructor (props) {
super(props);
bindAll(this, [
'handleHorizontalScrollbarMouseDown',
'handleHorizontalScrollbarMouseMove',
'handleHorizontalScrollbarMouseUp',
'handleVerticalScrollbarMouseDown',
'handleVerticalScrollbarMouseMove',
'handleVerticalScrollbarMouseUp',
'handleWheel'
]);
}
componentDidMount () {
if (this.props.canvas) {
this.props.canvas.addEventListener('wheel', this.handleWheel);
}
}
componentWillReceiveProps (nextProps) {
if (nextProps.canvas) {
if (this.props.canvas) {
this.props.canvas.removeEventListener('wheel', this.handleWheel);
}
nextProps.canvas.addEventListener('wheel', this.handleWheel);
}
}
handleHorizontalScrollbarMouseDown (event) {
this.initialMouseX = getEventXY(event).x;
this.initialScreenX = paper.view.matrix.tx;
window.addEventListener('mousemove', this.handleHorizontalScrollbarMouseMove);
window.addEventListener('mouseup', this.handleHorizontalScrollbarMouseUp);
event.preventDefault();
}
handleHorizontalScrollbarMouseMove (event) {
const dx = this.initialMouseX - getEventXY(event).x;
paper.view.matrix.tx = this.initialScreenX + (dx * paper.view.zoom * 2);
clampViewBounds();
this.props.updateViewBounds(paper.view.matrix);
event.preventDefault();
}
handleHorizontalScrollbarMouseUp () {
window.removeEventListener('mousemove', this.handleHorizontalScrollbarMouseMove);
window.removeEventListener('mouseup', this.handleHorizontalScrollbarMouseUp);
this.initialMouseX = null;
this.initialScreenX = null;
event.preventDefault();
}
handleVerticalScrollbarMouseDown (event) {
this.initialMouseY = getEventXY(event).y;
this.initialScreenY = paper.view.matrix.ty;
window.addEventListener('mousemove', this.handleVerticalScrollbarMouseMove);
window.addEventListener('mouseup', this.handleVerticalScrollbarMouseUp);
event.preventDefault();
}
handleVerticalScrollbarMouseMove (event) {
const dy = this.initialMouseY - getEventXY(event).y;
paper.view.matrix.ty = this.initialScreenY + (dy * paper.view.zoom * 2);
clampViewBounds();
this.props.updateViewBounds(paper.view.matrix);
event.preventDefault();
}
handleVerticalScrollbarMouseUp (event) {
window.removeEventListener('mousemove', this.handleVerticalScrollbarMouseMove);
window.removeEventListener('mouseup', this.handleVerticalScrollbarMouseUp);
this.initialMouseY = null;
this.initialScreenY = null;
event.preventDefault();
}
handleWheel (event) {
// Multiplier variable, so that non-pixel-deltaModes are supported. Needed for Firefox.
// See #529 (or LLK/scratch-blocks#1190).
const multiplier = event.deltaMode === 0x1 ? 15 : 1;
const deltaX = event.deltaX * multiplier;
const deltaY = event.deltaY * multiplier;
if (event.metaKey || event.ctrlKey) {
// Zoom keeping mouse location fixed
const canvasRect = this.props.canvas.getBoundingClientRect();
const offsetX = event.clientX - canvasRect.left;
const offsetY = event.clientY - canvasRect.top;
const fixedPoint = paper.view.viewToProject(
new paper.Point(offsetX, offsetY)
);
zoomOnFixedPoint(-deltaY / 1000, fixedPoint);
this.props.updateViewBounds(paper.view.matrix);
this.props.redrawSelectionBox(); // Selection handles need to be resized after zoom
} else if (event.shiftKey && event.deltaX === 0) {
// Scroll horizontally (based on vertical scroll delta)
// This is needed as for some browser/system combinations which do not set deltaX.
// See #156.
const dx = deltaY / paper.view.zoom;
pan(dx, 0);
this.props.updateViewBounds(paper.view.matrix);
} else {
const dx = deltaX / paper.view.zoom;
const dy = deltaY / paper.view.zoom;
pan(dx, dy);
this.props.updateViewBounds(paper.view.matrix);
}
event.preventDefault();
}
render () {
let widthPercent = 0;
let heightPercent = 0;
let topPercent = 0;
let leftPercent = 0;
if (paper.project) {
const {x, y, width, height} = paper.view.bounds;
widthPercent = Math.min(100, 100 * width / ART_BOARD_WIDTH);
heightPercent = Math.min(100, 100 * height / ART_BOARD_HEIGHT);
const centerX = (x + (width / 2)) / ART_BOARD_WIDTH;
const centerY = (y + (height / 2)) / ART_BOARD_HEIGHT;
topPercent = Math.max(0, (100 * centerY) - (heightPercent / 2));
leftPercent = Math.max(0, (100 * centerX) - (widthPercent / 2));
}
return (
<ScrollableCanvasComponent
hideCursor={this.props.hideCursor}
horizontalScrollLengthPercent={widthPercent}
horizontalScrollStartPercent={leftPercent}
style={this.props.style}
verticalScrollLengthPercent={heightPercent}
verticalScrollStartPercent={topPercent}
onHorizontalScrollbarMouseDown={this.handleHorizontalScrollbarMouseDown}
onVerticalScrollbarMouseDown={this.handleVerticalScrollbarMouseDown}
>
{this.props.children}
</ScrollableCanvasComponent>
);
}
}
ScrollableCanvas.propTypes = {
canvas: PropTypes.instanceOf(Element),
children: PropTypes.node.isRequired,
hideCursor: PropTypes.bool,
redrawSelectionBox: PropTypes.func.isRequired,
style: PropTypes.string,
updateViewBounds: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
viewBounds: state.scratchPaint.viewBounds
});
const mapDispatchToProps = dispatch => ({
redrawSelectionBox: () => {
dispatch(redrawSelectionBox());
},
updateViewBounds: matrix => {
dispatch(updateViewBounds(matrix));
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ScrollableCanvas);

View file

@ -9,7 +9,7 @@ const SVG_ART_BOARD_HEIGHT = 360;
const ART_BOARD_WIDTH = 480 * 2; const ART_BOARD_WIDTH = 480 * 2;
const ART_BOARD_HEIGHT = 360 * 2; const ART_BOARD_HEIGHT = 360 * 2;
const _clampViewBounds = () => { const clampViewBounds = () => {
const {left, right, top, bottom} = paper.project.view.bounds; const {left, right, top, bottom} = paper.project.view.bounds;
if (left < 0) { if (left < 0) {
paper.project.view.scrollBy(new paper.Point(-left, 0)); paper.project.view.scrollBy(new paper.Point(-left, 0));
@ -37,7 +37,7 @@ const zoomOnFixedPoint = (deltaZoom, fixedPoint) => {
.subtract(preZoomCenter); .subtract(preZoomCenter);
view.zoom = newZoom; view.zoom = newZoom;
view.translate(postZoomOffset.multiply(-1)); view.translate(postZoomOffset.multiply(-1));
_clampViewBounds(); clampViewBounds();
}; };
// Zoom keeping the selection center (if any) fixed. // Zoom keeping the selection center (if any) fixed.
@ -62,12 +62,12 @@ const zoomOnSelection = deltaZoom => {
const resetZoom = () => { const resetZoom = () => {
paper.project.view.zoom = .5; paper.project.view.zoom = .5;
_clampViewBounds(); clampViewBounds();
}; };
const pan = (dx, dy) => { const pan = (dx, dy) => {
paper.project.view.scrollBy(new paper.Point(dx, dy)); paper.project.view.scrollBy(new paper.Point(dx, dy));
_clampViewBounds(); clampViewBounds();
}; };
export { export {
@ -75,6 +75,7 @@ export {
ART_BOARD_WIDTH, ART_BOARD_WIDTH,
SVG_ART_BOARD_WIDTH, SVG_ART_BOARD_WIDTH,
SVG_ART_BOARD_HEIGHT, SVG_ART_BOARD_HEIGHT,
clampViewBounds,
pan, pan,
resetZoom, resetZoom,
zoomOnSelection, zoomOnSelection,

View file

@ -1,10 +1,14 @@
import log from '../log/log'; import log from '../log/log';
const CHANGE_SELECTED_ITEMS = 'scratch-paint/select/CHANGE_SELECTED_ITEMS'; const CHANGE_SELECTED_ITEMS = 'scratch-paint/select/CHANGE_SELECTED_ITEMS';
const REDRAW_SELECTION_BOX = 'scratch-paint/select/REDRAW_SELECTION_BOX';
const initialState = []; const initialState = [];
const reducer = function (state, action) { const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState; if (typeof state === 'undefined') state = initialState;
switch (action.type) { switch (action.type) {
case REDRAW_SELECTION_BOX:
if (state.length > 0) return state.slice(0); // Sends an update even though the items haven't changed
return state;
case CHANGE_SELECTED_ITEMS: case CHANGE_SELECTED_ITEMS:
if (!action.selectedItems || !(action.selectedItems instanceof Array)) { if (!action.selectedItems || !(action.selectedItems instanceof Array)) {
log.warn(`No selected items or wrong format provided: ${action.selectedItems}`); log.warn(`No selected items or wrong format provided: ${action.selectedItems}`);
@ -44,9 +48,15 @@ const clearSelectedItems = function () {
selectedItems: [] selectedItems: []
}; };
}; };
const redrawSelectionBox = function () {
return {
type: REDRAW_SELECTION_BOX
};
};
export { export {
reducer as default, reducer as default,
redrawSelectionBox,
setSelectedItems, setSelectedItems,
clearSelectedItems, clearSelectedItems,
CHANGE_SELECTED_ITEMS CHANGE_SELECTED_ITEMS