diff --git a/src/components/stage/stage.css b/src/components/stage/stage.css index dfce6ba24044ccbc6ff026947cd1ed3cbf3677b7..17c6e87b4b6504b6ebecfb621a8cc4f8378805d6 100644 --- a/src/components/stage/stage.css +++ b/src/components/stage/stage.css @@ -8,6 +8,10 @@ */ display: block; + /* Attach border radius directly to canvas to prevent needing overflow:hidden; */ + border-radius: $space; + border: 1px solid $ui-black-transparent; + /* @todo: This is for overriding the value being set somewhere. Where is it being set? */ background-color: transparent; } @@ -30,10 +34,6 @@ .stage-wrapper { position: relative; - border-radius: $space; - border: 1px solid $ui-black-transparent; - /* Keep the canvas inside the border radius */ - overflow: hidden; } .stage-wrapper-overlay { @@ -78,3 +78,11 @@ pointer-events: none; overflow: hidden; } + +.dragging-sprite { + position: absolute; + top: 0; + left: 0; + z-index: 1000; /* Above everything so it is draggable into other panes */ + filter: drop-shadow(5px 5px 5px $ui-black-transparent); + } diff --git a/src/components/stage/stage.jsx b/src/components/stage/stage.jsx index f79bd3d772709b59dd988c4fa16bd284b808e4d9..37f12706b0cc0de16bb334dc1fc7f8b8d6e8d5d4 100644 --- a/src/components/stage/stage.jsx +++ b/src/components/stage/stage.jsx @@ -12,6 +12,7 @@ import styles from './stage.css'; const StageComponent = props => { const { canvasRef, + dragRef, height, isColorPicking, isFullScreen, @@ -24,7 +25,7 @@ const StageComponent = props => { } = props; const stageSize = getStageSize(isFullScreen, height, width); - + return ( <div> <Box @@ -71,6 +72,12 @@ const StageComponent = props => { </div> </div> )} + <canvas + className={styles.draggingSprite} + height={0} + ref={dragRef} + width={0} + /> </Box> {isColorPicking ? ( <Box @@ -84,6 +91,7 @@ const StageComponent = props => { StageComponent.propTypes = { canvasRef: PropTypes.func, colorInfo: Loupe.propTypes.colorInfo, + dragRef: PropTypes.func, height: PropTypes.number, isColorPicking: PropTypes.bool, isFullScreen: PropTypes.bool.isRequired, @@ -94,6 +102,7 @@ StageComponent.propTypes = { }; StageComponent.defaultProps = { canvasRef: () => {}, + dragRef: () => {}, width: 480, height: 360 }; diff --git a/src/containers/stage.jsx b/src/containers/stage.jsx index cf10d50019342bdf9942d36dfb580d7a6f6ff9f0..688951f73e858b61c583c624fe45d716df010fb4 100644 --- a/src/containers/stage.jsx +++ b/src/containers/stage.jsx @@ -34,7 +34,11 @@ class Stage extends React.Component { 'onWheel', 'updateRect', 'questionListener', - 'setCanvas' + 'setCanvas', + 'setDragCanvas', + 'clearDragCanvas', + 'drawDragCanvas', + 'positionDragCanvas' ]); this.state = { mouseDownTimeoutId: null, @@ -170,6 +174,7 @@ class Stage extends React.Component { y: -(spritePosition[1] + this.state.dragOffset[1]), force: true }); + this.positionDragCanvas(mousePosition[0], mousePosition[1]); } const coordinates = { x: mousePosition[0], @@ -248,11 +253,47 @@ class Stage extends React.Component { } this.setState({mouseDownTimeoutId: null}); } + drawDragCanvas (drawableData) { + const { + data, + width, + height, + x, + y + } = drawableData; + this.dragCanvas.width = width; + this.dragCanvas.height = height; + // Need to convert uint8array from WebGL readPixels into Uint8ClampedArray + // for ImageData constructor. Shares underlying buffer, so it is fast. + const imageData = new ImageData( + new Uint8ClampedArray(data.buffer), width, height); + this.dragCanvas.getContext('2d').putImageData(imageData, 0, 0); + // Position so that pick location is at (0, 0) so that positionDragCanvas() + // can use translation to move to mouse position smoothly. + this.dragCanvas.style.left = `${-x}px`; + this.dragCanvas.style.top = `${-y}px`; + } + clearDragCanvas () { + this.dragCanvas.width = this.dragCanvas.height = 0; + } + positionDragCanvas (mouseX, mouseY) { + // mouseX/Y are relative to stage top/left, and dragCanvas is already + // positioned so that the pick location is at (0,0). + this.dragCanvas.style.transform = `translate(${mouseX}px, ${mouseY}px)`; + } onStartDrag (x, y) { + if (this.state.dragId) return; const drawableId = this.renderer.pick(x, y); if (drawableId === null) return; const drawableData = this.renderer.extractDrawable(drawableId, x, y); const targetId = this.props.vm.getTargetIdForDrawableId(drawableId); + + // Only start drags on non-draggable targets in editor drag mode + if (!this.props.useEditorDragStyle) { + const target = this.props.vm.runtime.getTargetById(targetId); + if (!target.draggable) return; + } + if (targetId === null) return; this.props.vm.startDrag(targetId); this.setState({ @@ -260,18 +301,41 @@ class Stage extends React.Component { dragId: targetId, dragOffset: drawableData.scratchOffset }); + if (this.props.useEditorDragStyle) { + this.drawDragCanvas(drawableData); + this.positionDragCanvas(x, y); + this.props.vm.postSpriteInfo({visible: false}); + } } onStopDrag () { - this.props.vm.stopDrag(this.state.dragId); - this.setState({ - isDragging: false, - dragOffset: null, - dragId: null - }); + const dragId = this.state.dragId; + const commonStopDragActions = () => { + this.props.vm.stopDrag(dragId); + this.setState({ + isDragging: false, + dragOffset: null, + dragId: null + }); + }; + if (this.props.useEditorDragStyle) { + // Need to sequence these actions to prevent flickering. + this.props.vm.postSpriteInfo({visible: true}); + setTimeout(() => { + this.clearDragCanvas(); + setTimeout(() => { + commonStopDragActions(); + }, 30); + }, 30); + } else { + commonStopDragActions(); + } } setCanvas (canvas) { this.canvas = canvas; } + setDragCanvas (canvas) { + this.dragCanvas = canvas; + } render () { const { vm, // eslint-disable-line no-unused-vars @@ -282,6 +346,7 @@ class Stage extends React.Component { <StageComponent canvasRef={this.setCanvas} colorInfo={this.state.colorInfo} + dragRef={this.setDragCanvas} question={this.state.question} onDoubleClick={this.handleDoubleClick} onQuestionAnswered={this.handleQuestionAnswered} @@ -297,13 +362,20 @@ Stage.propTypes = { isFullScreen: PropTypes.bool.isRequired, onActivateColorPicker: PropTypes.func, onDeactivateColorPicker: PropTypes.func, + useEditorDragStyle: PropTypes.bool, vm: PropTypes.instanceOf(VM).isRequired, width: PropTypes.number }; +Stage.defaultProps = { + useEditorDragStyle: true +}; + const mapStateToProps = state => ({ isColorPicking: state.colorPicker.active, - isFullScreen: state.stageSize.isFullScreen + isFullScreen: state.stageSize.isFullScreen, + // Do not use editor drag style in fullscreen mode. + useEditorDragStyle: !state.stageSize.isFullScreen }); const mapDispatchToProps = dispatch => ({