Skip to content
Snippets Groups Projects
Commit bd0e3295 authored by Paul Kaplan's avatar Paul Kaplan
Browse files

Add direction popover with dial and rotation style changing

parent 3037745f
No related branches found
No related tags found
No related merge requests found
Showing
with 455 additions and 21 deletions
@import "../../css/colors.css";
.container {
padding: 1rem;
display: flex;
justify-content: center;
align-items: center;
}
.dial-container {
position: relative;
}
.dial-face, .dial-handle, .gauge {
position: absolute;
top: 0;
left: 0;
overflow: visible;
}
.dial-face {
width: 100%;
}
$dial-size: 40px;
.dial-handle {
cursor: pointer;
width: $dial-size;
height: $dial-size;
/* Use margin to make positioning via top/left easier */
margin-left: calc($dial-size / -2);
margin-top: calc($dial-size / -2);
}
.gauge-path {
fill: $motion-transparent;
stroke: $motion-primary;
stroke-width: 1px;
}
import PropTypes from 'prop-types';
import bindAll from 'lodash.bindall';
import React from 'react';
import {getEventXY} from '../../lib/touch-utils';
import styles from './dial.css';
import dialFace from './icon--dial.svg';
import dialHandle from './icon--handle.svg';
class Dial extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleMouseDown',
'handleMouseMove',
'containerRef',
'handleRef',
'unbindMouseEvents'
]);
}
componentDidMount () {
// Manually add touch/mouse handlers so that preventDefault can be used
// to prevent scrolling on touch.
// Tracked as a react issue https://github.com/facebook/react/issues/6436
this.handleElement.addEventListener('mousedown', this.handleMouseDown);
this.handleElement.addEventListener('touchstart', this.handleMouseDown);
}
componentWillUnmount () {
this.unbindMouseEvents();
this.handleElement.removeEventListener('mousedown', this.handleMouseDown);
this.handleElement.removeEventListener('touchstart', this.handleMouseDown);
}
/**
* Get direction from dial center to mouse move event.
* @param {Event} e - Mouse move event.
* @returns {number} Direction in degrees, clockwise, 90=horizontal.
*/
directionToMouseEvent (e) {
const {x: mx, y: my} = getEventXY(e);
const bbox = this.containerElement.getBoundingClientRect();
const cy = bbox.top + (bbox.height / 2);
const cx = bbox.left + (bbox.width / 2);
const angle = Math.atan2(my - cy, mx - cx);
const degrees = angle * (180 / Math.PI);
return degrees + 90; // To correspond with scratch coordinate system
}
/**
* Create SVG path data string for the dial "gauge", the overlaid arc slice.
* @param {number} radius - The radius of the dial.
* @param {number} direction - Direction in degrees, clockwise, 90=horizontal.
* @returns {string} Path data string for the gauge.
*/
gaugePath (radius, direction) {
const rads = (direction) * (Math.PI / 180)
const path = [];
path.push(`M ${radius} 0`);
path.push(`L ${radius} ${radius}`);
path.push(`L ${radius + radius * Math.sin(rads)} ${radius - radius * Math.cos(rads)}`);
path.push(`A ${radius} ${radius} 0 0 ${direction < 0 ? 1 : 0} ${radius} 0`);
path.push(`Z`);
return path.join(' ');
}
handleMouseMove (e) {
this.props.onChange(this.directionToMouseEvent(e) + this.directionOffset);
e.preventDefault();
}
unbindMouseEvents () {
window.removeEventListener('mousemove', this.handleMouseMove);
window.removeEventListener('mouseup', this.unbindMouseEvents);
window.removeEventListener('touchmove', this.handleMouseMove);
window.removeEventListener('touchend', this.unbindMouseEvents);
}
handleMouseDown (e) {
// Because the drag handle is not a single point, there is some initial
// difference between the current sprite direction and the direction to the mouse
// Store this offset to prevent jumping when the mouse is moved.
this.directionOffset = this.props.direction - this.directionToMouseEvent(e);
window.addEventListener('mousemove', this.handleMouseMove);
window.addEventListener('mouseup', this.unbindMouseEvents);
window.addEventListener('touchmove', this.handleMouseMove);
window.addEventListener('touchend', this.unbindMouseEvents);
e.preventDefault();
}
containerRef (el) {
this.containerElement = el;
}
handleRef (el) {
this.handleElement = el;
}
render () {
const {direction, radius} = this.props;
return (
<div className={styles.container}>
<div
className={styles.dialContainer}
style={{
width: `${radius * 2}px`,
height: `${radius * 2}px`,
}}
ref={this.containerRef}
>
<img
src={dialFace}
className={styles.dialFace}
draggable={false}
/>
<svg
width={radius * 2}
height={radius * 2}
className={styles.gauge}
>
<path
className={styles.gaugePath}
d={this.gaugePath(radius, direction)}
/>
</svg>
<img
src={dialHandle}
className={styles.dialHandle}
draggable={false}
style={{
top: `${radius - radius * Math.cos(direction * (Math.PI / 180))}px`,
left: `${radius + radius * Math.sin(direction * (Math.PI / 180))}px`,
transform: `rotate(${direction}deg)`
}}
ref={this.handleRef}
/>
</div>
</div>
);
}
}
Dial.propTypes = {
direction: PropTypes.number,
onChange: PropTypes.func.isRequired,
radius: PropTypes.number
};
Dial.defaultProps = {
radius: 56, // px
direction: 90 // degrees
};
export default Dial;
@import "../../css/colors.css";
.button-row {
display: flex;
flex-direction: row;
justify-content: center;
}
.icon-button {
margin: 0.25rem;
border: none;
background: none;
outline: none;
cursor: pointer;
}
.icon-button:active > img {
width: 20px;
height: 20px;
transform: scale(1.15);
}
.icon-button > img {
transition: transform 0.1s;
filter: grayscale(100%);
}
.icon-button.active > img {
filter: none;
}
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Popover from 'react-popover';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
import Label from '../forms/label.jsx';
import Input from '../forms/input.jsx';
import BufferedInputHOC from '../forms/buffered-input-hoc.jsx';
import Dial from './dial.jsx';
import styles from './direction-picker.css';
import allAroundIcon from './icon--all-around.svg';
import leftRightIcon from './icon--left-right.svg';
import dontRotateIcon from './icon--dont-rotate.svg';
const BufferedInput = BufferedInputHOC(Input);
const directionLabel = (
<FormattedMessage
defaultMessage="Direction"
description="Sprite info direction label"
id="gui.SpriteInfo.direction"
/>
);
const RotationStyles = {
ALL_AROUND: 'all around',
LEFT_RIGHT: 'left-right',
DONT_ROTATE: "don't rotate"
};
const messages = defineMessages({
allAround: {
id: 'gui.directionPicker.rotationStyles.allAround',
description: 'Button to change to the all around rotation style',
defaultMessage: 'All Around'
},
leftRight: {
id: 'gui.directionPicker.rotationStyles.leftRight',
description: 'Button to change to the left-right rotation style',
defaultMessage: 'Left/Right'
},
dontRotate: {
id: 'gui.directionPicker.rotationStyles.dontRotate',
description: 'Button to change to the dont rotate rotation style',
defaultMessage: 'Do not rotate'
}
});
const DirectionPicker = props => (
<Label
secondary
text={directionLabel}
>
<Popover
body={
<div>
<Dial
direction={props.direction}
onChange={props.onChangeDirection}
/>
<div className={styles.buttonRow}>
<button
className={classNames(styles.iconButton, {
[styles.active]: props.rotationStyle === RotationStyles.ALL_AROUND
})}
title={props.intl.formatMessage(messages.allAround)}
onClick={props.onClickAllAround}
>
<img
draggable={false}
src={allAroundIcon}
/>
</button>
<button
className={classNames(styles.iconButton, {
[styles.active]: props.rotationStyle === RotationStyles.LEFT_RIGHT
})}
title={props.intl.formatMessage(messages.leftRight)}
onClick={props.onClickLeftRight}
>
<img
draggable={false}
src={leftRightIcon}
/>
</button>
<button
className={classNames(styles.iconButton, {
[styles.active]: props.rotationStyle === RotationStyles.DONT_ROTATE
})}
title={props.intl.formatMessage(messages.dontRotate)}
onClick={props.onClickDontRotate}
>
<img
draggable={false}
src={dontRotateIcon}
/>
</button>
</div>
</div>
}
isOpen={props.popoverOpen}
preferPlace="above"
onOuterAction={props.onClosePopover}
>
<BufferedInput
small
disabled={props.disabled}
label={directionLabel}
tabIndex="0"
type="text"
value={props.disabled ? '' : props.direction}
onFocus={props.onOpenPopover}
onSubmit={props.onChangeDirection}
/>
</Popover>
</Label>
);
DirectionPicker.propTypes = {
direction: PropTypes.number.isRequired,
disabled: PropTypes.bool.isRequired,
intl: intlShape,
onChangeDirection: PropTypes.func.isRequired,
onClickAllAround: PropTypes.func.isRequired,
onClickDontRotate: PropTypes.func.isRequired,
onClickLeftRight: PropTypes.func.isRequired,
onClosePopover: PropTypes.func.isRequired,
onOpenPopover: PropTypes.func.isRequired,
popoverOpen: PropTypes.bool.isRequired,
rotationStyle: PropTypes.string
};
const WrappedDirectionPicker = injectIntl(DirectionPicker);
export {
WrappedDirectionPicker as default,
RotationStyles
};
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
......@@ -6,6 +6,8 @@ import Box from '../box/box.jsx';
import Label from '../forms/label.jsx';
import Input from '../forms/input.jsx';
import BufferedInputHOC from '../forms/buffered-input-hoc.jsx';
import DirectionPicker from '../../containers/direction-picker.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
import {STAGE_DISPLAY_SIZES} from '../../lib/layout-constants.js';
......@@ -29,6 +31,7 @@ const messages = defineMessages({
class SpriteInfo extends React.Component {
shouldComponentUpdate (nextProps) {
return (
this.props.rotationStyle !== nextProps.rotationStyle ||
this.props.direction !== nextProps.direction ||
this.props.disabled !== nextProps.disabled ||
this.props.name !== nextProps.name ||
......@@ -65,13 +68,6 @@ class SpriteInfo extends React.Component {
id="gui.SpriteInfo.size"
/>
);
const directionLabel = (
<FormattedMessage
defaultMessage="Direction"
description="Sprite info direction label"
id="gui.SpriteInfo.direction"
/>
);
const spriteNameInput = (
<BufferedInput
......@@ -234,20 +230,13 @@ class SpriteInfo extends React.Component {
</Label>
</div>
<div className={classNames(styles.group, styles.largerInput)}>
<Label
secondary
text={directionLabel}
>
<BufferedInput
small
disabled={this.props.disabled}
label={directionLabel}
tabIndex="0"
type="text"
value={this.props.disabled ? '' : this.props.direction}
onSubmit={this.props.onChangeDirection}
/>
</Label>
<DirectionPicker
direction={this.props.direction}
disabled={this.props.disabled}
rotationStyle={this.props.rotationStyle}
onChangeDirection={this.props.onChangeDirection}
onChangeRotationStyle={this.props.onChangeRotationStyle}
/>
</div>
</div>
</Box>
......@@ -265,6 +254,7 @@ SpriteInfo.propTypes = {
name: PropTypes.string,
onChangeDirection: PropTypes.func,
onChangeName: PropTypes.func,
onChangeRotationStyle: PropTypes.func,
onChangeSize: PropTypes.func,
onChangeX: PropTypes.func,
onChangeY: PropTypes.func,
......@@ -272,6 +262,7 @@ SpriteInfo.propTypes = {
onClickVisible: PropTypes.func,
onPressNotVisible: PropTypes.func,
onPressVisible: PropTypes.func,
rotationStyle: PropTypes.string,
size: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
......
......@@ -47,6 +47,7 @@ const SpriteSelectorComponent = function (props) {
intl,
onChangeSpriteDirection,
onChangeSpriteName,
onChangeSpriteRotationStyle,
onChangeSpriteSize,
onChangeSpriteVisibility,
onChangeSpriteX,
......@@ -84,6 +85,7 @@ const SpriteSelectorComponent = function (props) {
direction={selectedSprite.direction}
disabled={spriteInfoDisabled}
name={selectedSprite.name}
rotationStyle={selectedSprite.rotationStyle}
size={selectedSprite.size}
stageSize={stageSize}
visible={selectedSprite.visible}
......@@ -91,6 +93,7 @@ const SpriteSelectorComponent = function (props) {
y={selectedSprite.y}
onChangeDirection={onChangeSpriteDirection}
onChangeName={onChangeSpriteName}
onChangeRotationStyle={onChangeSpriteRotationStyle}
onChangeSize={onChangeSpriteSize}
onChangeVisibility={onChangeSpriteVisibility}
onChangeX={onChangeSpriteX}
......@@ -152,6 +155,7 @@ SpriteSelectorComponent.propTypes = {
intl: intlShape.isRequired,
onChangeSpriteDirection: PropTypes.func,
onChangeSpriteName: PropTypes.func,
onChangeSpriteRotationStyle: PropTypes.func,
onChangeSpriteSize: PropTypes.func,
onChangeSpriteVisibility: PropTypes.func,
onChangeSpriteX: PropTypes.func,
......
......@@ -23,6 +23,7 @@ const TargetPane = ({
spriteLibraryVisible,
onChangeSpriteDirection,
onChangeSpriteName,
onChangeSpriteRotationStyle,
onChangeSpriteSize,
onChangeSpriteVisibility,
onChangeSpriteX,
......@@ -60,6 +61,7 @@ const TargetPane = ({
stageSize={stageSize}
onChangeSpriteDirection={onChangeSpriteDirection}
onChangeSpriteName={onChangeSpriteName}
onChangeSpriteRotationStyle={onChangeSpriteRotationStyle}
onChangeSpriteSize={onChangeSpriteSize}
onChangeSpriteVisibility={onChangeSpriteVisibility}
onChangeSpriteX={onChangeSpriteX}
......@@ -128,6 +130,7 @@ TargetPane.propTypes = {
}),
onChangeSpriteDirection: PropTypes.func,
onChangeSpriteName: PropTypes.func,
onChangeSpriteRotationStyle: PropTypes.func,
onChangeSpriteSize: PropTypes.func,
onChangeSpriteVisibility: PropTypes.func,
onChangeSpriteX: PropTypes.func,
......
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import DirectionComponent, {RotationStyle} from '../components/direction-picker/direction-picker.jsx';
class DirectionPicker extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleOpenPopover',
'handleClosePopover',
'handleClickLeftRight',
'handleClickDontRotate',
'handleClickAllAround'
]);
this.state = {
popoverOpen: false
};
}
handleOpenPopover () {
this.setState({popoverOpen: true});
}
handleClosePopover () {
this.setState({popoverOpen: false});
}
handleClickAllAround () {
this.props.onChangeRotationStyle(RotationStyle.ALL_AROUND);
}
handleClickLeftRight () {
this.props.onChangeRotationStyle(RotationStyle.LEFT_RIGHT);
}
handleClickDontRotate () {
this.props.onChangeRotationStyle(RotationStyle.DONT_ROTATE);
}
render () {
return (
<DirectionComponent
direction={this.props.direction}
disabled={this.props.disabled}
popoverOpen={this.state.popoverOpen && !this.props.disabled}
rotationStyle={this.props.rotationStyle}
onChangeDirection={this.props.onChangeDirection}
onClickAllAround={this.handleClickAllAround}
onClickDontRotate={this.handleClickDontRotate}
onClickLeftRight={this.handleClickLeftRight}
onClosePopover={this.handleClosePopover}
onOpenPopover={this.handleOpenPopover}
/>
);
}
}
DirectionPicker.propTypes = {
direction: PropTypes.number,
disabled: PropTypes.bool,
onChangeDirection: PropTypes.func,
onChangeRotationStyle: PropTypes.func,
rotationStyle: PropTypes.string
};
export default DirectionPicker;
......@@ -20,6 +20,7 @@ class TargetPane extends React.Component {
super(props);
bindAll(this, [
'handleBlockDragEnd',
'handleChangeSpriteRotationStyle',
'handleChangeSpriteDirection',
'handleChangeSpriteName',
'handleChangeSpriteSize',
......@@ -48,6 +49,9 @@ class TargetPane extends React.Component {
handleChangeSpriteDirection (direction) {
this.props.vm.postSpriteInfo({direction});
}
handleChangeSpriteRotationStyle (rotationStyle) {
this.props.vm.postSpriteInfo({rotationStyle});
}
handleChangeSpriteName (name) {
this.props.vm.renameSprite(this.props.editingTarget, name);
}
......@@ -165,6 +169,7 @@ class TargetPane extends React.Component {
<TargetPaneComponent
{...componentProps}
fileInputRef={this.setFileInput}
onChangeSpriteRotationStyle={this.handleChangeSpriteRotationStyle}
onChangeSpriteDirection={this.handleChangeSpriteDirection}
onChangeSpriteName={this.handleChangeSpriteName}
onChangeSpriteSize={this.handleChangeSpriteSize}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment