Compare commits

...
Author SHA1 Message Date
rschamp f67cf3c272 Disable wobbles on Edge 2020-03-30 14:30:51 -04:00
DD Liu 9a85d2466f Merge pull request #5551 from LLK/revert-5548-revert-5544-wobbly-dragging
Revert "Revert "Make sprites wobbly when dragged""
2020-03-30 12:50:25 -04:00
adroitwhiz 3ece0e1255 Use position: fixed to prevent overflow scrollbar 2020-03-30 11:56:30 -04:00
adroitwhiz 729395b8d4 Remove flex overflow 2020-03-30 11:56:30 -04:00
adroitwhiz 39f85aa4f1 Clear color 2020-03-30 11:56:29 -04:00
DD Liu a00f86bf16 Revert "Revert "Make sprites wobbly when dragged"" 2020-03-30 11:50:06 -04:00
DD Liu 6fe78429a5 Merge pull request #5546 from fsih/catBlocks
Add cat blocks performance and RTL updates
2020-03-27 13:53:39 -04:00
DD Liu 9138e1e164 update blocks 2020-03-27 13:43:46 -04:00
DD Liu c75a91ac29 Merge branch 'hotfix/totally-normal-2020' of github.com:LLK/scratch-gui into catBlocks 2020-03-27 13:08:41 -04:00
Ray Schamp a554acf667 Merge pull request #5548 from LLK/revert-5544-wobbly-dragging
Revert "Make sprites wobbly when dragged"
2020-03-27 12:57:23 -04:00
Ray Schamp 782681dcc4 Revert "Make sprites wobbly when dragged" 2020-03-27 12:56:22 -04:00
DD Liu 3c7193b562 Add cat blocks 2020-03-27 11:59:57 -04:00
Ray Schamp 0ed7f78abb Merge pull request #5544 from adroitwhiz/wobbly-dragging
Make sprites wobbly when dragged
2020-03-27 09:46:55 -04:00
adroitwhiz 737ce711e5 Make dragged sprites wobbly 2020-03-27 09:10:01 -04:00
rschamp 533408409f Merge branch 'develop' of github.com:LLK/scratch-gui into hotfix/totally-normal-2020
# Conflicts:
#	package-lock.json
#	package.json
2020-03-26 20:37:40 -04:00
rschamp a99b5fcbf9 Bump blocks 2020-03-25 21:20:23 -04:00
Ray Schamp a08bb42658 Merge pull request #5537 from adroitwhiz/hotfix/totally-normal-2020
Demystify non-editor mode
2020-03-25 15:46:59 -04:00
adroitwhiz 3e5f7e917a Demystify editor mode 2020-03-24 15:43:27 -04:00
rschamp 3014e56e24 Merge branch 'very-normal' of github.com:zoebentley/scratch-gui into hotfix/totally-normal-2020 2020-03-24 09:38:48 -04:00
Ray Schamp a8cd6f9c69 Update package.json 2020-03-24 09:35:59 -04:00
adroitwhiz c04acb513a Fix mystery on Edge 2020-03-24 09:34:27 -04:00
adroitwhiz 8ce4322051 Enhance mysterious atmosphere 2020-03-24 09:34:27 -04:00
rschamp f0637a40c5 Add mystery mode 2020-03-24 09:29:23 -04:00
Zoë Bentley a6cd49d400 Totally normal 2020-03-20 13:40:22 -04:00
20 changed files with 746 additions and 50 deletions

No files matched your search

+26 -26
View File
@@ -4860,27 +4860,27 @@
"dev": true
},
"falafel": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/falafel/-/falafel-2.1.0.tgz",
"integrity": "sha1-lrsXdh2rqU9G0AFzizzt86Z/4Gw=",
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/falafel/-/falafel-2.2.4.tgz",
"integrity": "sha512-0HXjo8XASWRmsS0X1EkhwEMZaD3Qvp7FfURwjLKjG1ghfRm/MGZl2r4cWUTv41KdNghTw4OUMmVtdGQp3+H+uQ==",
"dev": true,
"requires": {
"acorn": "^5.0.0",
"acorn": "^7.1.1",
"foreach": "^2.0.5",
"isarray": "0.0.1",
"isarray": "^2.0.1",
"object-keys": "^1.0.6"
},
"dependencies": {
"acorn": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
"integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
"integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
"dev": true
},
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
"dev": true
}
}
@@ -10649,9 +10649,9 @@
},
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
}
}
@@ -11549,9 +11549,9 @@
}
},
"scratch-blocks": {
"version": "0.1.0-prerelease.1585138708",
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.1585138708.tgz",
"integrity": "sha512-36UhpFoqVZg79nvZi9lxn0nmZ5hOQqIyIELMmogfwMl5CsRE4HfcjiLizDY/ORm6H5zH/AS1SFmiG78YNCnN+A==",
"version": "0.1.0-prerelease.1585323236",
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.1585323236.tgz",
"integrity": "sha512-iiadFomOLeKX+AU/cwBKTq0X/82zr/d+TcOorEpE0xiIfhztKV+cE5EiQA+eMfpQYZs8XBjojgAgs8Ob+zPuRw==",
"dev": true,
"requires": {
"exports-loader": "0.6.3",
@@ -11686,9 +11686,9 @@
}
},
"scratch-render": {
"version": "0.1.0-prerelease.20200228152431",
"resolved": "https://registry.npmjs.org/scratch-render/-/scratch-render-0.1.0-prerelease.20200228152431.tgz",
"integrity": "sha512-rI/IVwzAHVhq6IoFOgsqwwwwl9acizjJBVxOHKyP9F+/vs34uhbxr+mVUdpO82tSWpjDR+xuw3XfF7oH9U6WnQ==",
"version": "0.1.0-prerelease.20200324022200",
"resolved": "https://registry.npmjs.org/scratch-render/-/scratch-render-0.1.0-prerelease.20200324022200.tgz",
"integrity": "sha512-nW77KkYJa6YG9ytTs6v+DJ1cy6fc+E1B5Jm+K4735VTsrz3eej/APS0QuXx/TdcD29gJryOGkfAcAF4UkBq4lw==",
"dev": true,
"requires": {
"grapheme-breaker": "0.3.2",
@@ -11773,9 +11773,9 @@
"dev": true
},
"scratch-vm": {
"version": "0.2.0-prerelease.20200319175700",
"resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-0.2.0-prerelease.20200319175700.tgz",
"integrity": "sha512-CSCGU3XJhq68kw+74Nsdj3Bfo4D0jsGumh3DocLQCDbltrpvfEDfAMHwkFwetdCE8/XO+oHY/FdI4Joy7OimwA==",
"version": "0.2.0-prerelease.20200327113900",
"resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-0.2.0-prerelease.20200327113900.tgz",
"integrity": "sha512-CjnzBAT3GoP0g9X4QXkW2GP3AFRIoPTLwsR0luiN3VLYp4NOETsZpF+0uXfGzKp03M8lXH+YRsmlhsls8by1Pw==",
"dev": true,
"requires": {
"@vernier/godirect": "1.5.0",
@@ -12493,9 +12493,9 @@
"dev": true
},
"static-eval": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.3.tgz",
"integrity": "sha512-zsxDGucfAh8T339sSKgpFbvg15Fms2IVaJGC+jqp0bVsxhcpM+iMeAI8weNo8dmf4OblgifTBUoyk1vGVtYw2w==",
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.5.tgz",
"integrity": "sha512-nNbV6LbGtMBgv7e9LFkt5JV8RVlRsyJrphfAt9tOtBBW/SfnzZDf2KnS72an8e434A+9e/BmJuTxeGPvrAK7KA==",
"dev": true,
"requires": {
"escodegen": "^1.11.1"
+3 -3
View File
@@ -108,13 +108,13 @@
"redux-throttle": "0.1.1",
"rimraf": "^2.6.1",
"scratch-audio": "0.1.0-prerelease.20190925183642",
"scratch-blocks": "0.1.0-prerelease.1585138708",
"scratch-blocks": "0.1.0-prerelease.1585323236",
"scratch-l10n": "3.8.20200325112845",
"scratch-paint": "0.2.0-prerelease.20200213174123",
"scratch-render": "0.1.0-prerelease.20200228152431",
"scratch-render": "0.1.0-prerelease.20200324022200",
"scratch-storage": "1.3.2",
"scratch-svg-renderer": "0.2.0-prerelease.20200205003400",
"scratch-vm": "0.2.0-prerelease.20200319175700",
"scratch-vm": "0.2.0-prerelease.20200327113900",
"selenium-webdriver": "3.6.0",
"startaudiocontext": "1.2.1",
"style-loader": "^0.23.0",
+2
View File
@@ -18,6 +18,7 @@ import StageWrapper from '../../containers/stage-wrapper.jsx';
import Loader from '../loader/loader.jsx';
import Box from '../box/box.jsx';
import MenuBar from '../menu-bar/menu-bar.jsx';
import MysteryMode from '../mystery-mode/mystery-mode.jsx';
import CostumeLibrary from '../../containers/costume-library.jsx';
import BackdropLibrary from '../../containers/backdrop-library.jsx';
import Watermark from '../../containers/watermark.jsx';
@@ -156,6 +157,7 @@ const GUIComponent = props => {
dir={isRtl ? 'rtl' : 'ltr'}
{...componentProps}
>
<MysteryMode />
{telemetryModalVisible ? (
<TelemetryModal
onCancel={onTelemetryModalCancel}
+46 -2
View File
@@ -32,6 +32,8 @@ import MenuBarHOC from '../../containers/menu-bar-hoc.jsx';
import {openTipsLibrary} from '../../reducers/modals';
import {setPlayer} from '../../reducers/mode';
import {toggleMysteryMode} from '../../reducers/mystery-mode';
import {toggleWobblyDragging} from '../../reducers/wobbly-dragging';
import {
autoUpdateProject,
getIsUpdating,
@@ -74,6 +76,8 @@ import scratchLogo from './scratch-logo.svg';
import sharedMessages from '../../lib/shared-messages';
const isEdge = bowser.msedge;
const ariaMessages = defineMessages({
language: {
id: 'gui.menuBar.LanguageSelector',
@@ -469,6 +473,38 @@ class MenuBar extends React.Component {
)}
</MenuItem>
)}</TurboMode>
<MenuItem onClick={this.props.onClickToggleMysteryMode}>
{this.props.mysteryMode ? (
<FormattedMessage
defaultMessage="Turn off Mystery Mode"
description="Menu bar item for turning off dark mode"
id="gui.menuBar.mysteryModeOff"
/>
) : (
<FormattedMessage
defaultMessage="Turn on Mystery Mode"
description="Menu bar item for turning on dark mode"
id="gui.menuBar.mysteryModeOn"
/>
)}
</MenuItem>
{isEdge ? null : (
<MenuItem onClick={this.props.onClickToggleWobblyDragging}>
{this.props.wobblyDragging ? (
<FormattedMessage
defaultMessage="Turn off wobbly sprite dragging"
description="Menu bar item for turning off wobbly sprite dragging"
id="gui.menuBar.wobblyDraggingOff"
/>
) : (
<FormattedMessage
defaultMessage="Turn on wobbly sprite dragging"
description="Menu bar item for turning on wobbly sprite dragging"
id="gui.menuBar.wobblyDraggingOn"
/>
)}
</MenuItem>
)}
</MenuSection>
</MenuBarMenu>
</div>
@@ -722,6 +758,7 @@ MenuBar.propTypes = {
locale: PropTypes.string.isRequired,
loginMenuOpen: PropTypes.bool,
logo: PropTypes.string,
mysteryMode: PropTypes.bool,
onClickAccount: PropTypes.func,
onClickEdit: PropTypes.func,
onClickFile: PropTypes.func,
@@ -732,6 +769,8 @@ MenuBar.propTypes = {
onClickRemix: PropTypes.func,
onClickSave: PropTypes.func,
onClickSaveAsCopy: PropTypes.func,
onClickToggleMysteryMode: PropTypes.func,
onClickToggleWobblyDragging: PropTypes.func,
onLogOut: PropTypes.func,
onOpenRegistration: PropTypes.func,
onOpenTipLibrary: PropTypes.func,
@@ -751,7 +790,8 @@ MenuBar.propTypes = {
showComingSoon: PropTypes.bool,
userOwnsProject: PropTypes.bool,
username: PropTypes.string,
vm: PropTypes.instanceOf(VM).isRequired
vm: PropTypes.instanceOf(VM).isRequired,
wobblyDragging: PropTypes.bool
};
MenuBar.defaultProps = {
@@ -772,12 +812,14 @@ const mapStateToProps = (state, ownProps) => {
languageMenuOpen: languageMenuOpen(state),
locale: state.locales.locale,
loginMenuOpen: loginMenuOpen(state),
mysteryMode: state.scratchGui.mysteryMode,
projectTitle: state.scratchGui.projectTitle,
sessionExists: state.session && typeof state.session.session !== 'undefined',
username: user ? user.username : null,
userOwnsProject: ownProps.authorUsername && user &&
(ownProps.authorUsername === user.username),
vm: state.scratchGui.vm
vm: state.scratchGui.vm,
wobblyDragging: state.scratchGui.wobblyDragging
};
};
@@ -798,6 +840,8 @@ const mapDispatchToProps = dispatch => ({
onClickRemix: () => dispatch(remixProject()),
onClickSave: () => dispatch(manualUpdateProject()),
onClickSaveAsCopy: () => dispatch(saveProjectAsCopy()),
onClickToggleMysteryMode: () => dispatch(toggleMysteryMode()),
onClickToggleWobblyDragging: () => dispatch(toggleWobblyDragging()),
onSeeCommunity: () => dispatch(setPlayer(true))
});
@@ -0,0 +1,9 @@
.mystery-mode {
position: absolute;
width: 100%;
height: 100%;
opacity: 0.9;
background: transparent url('./mystery.png') center center;
z-index: 50;
pointer-events: none;
}
@@ -0,0 +1,55 @@
import PropTypes from 'prop-types';
import React from 'react';
import bindAll from 'lodash.bindall';
import styles from './mystery-mode.css';
import {connect} from 'react-redux';
class MysteryMode extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleMouseMove'
]);
this.state = {
x: window.innerWidth / 2,
y: window.innerHeight / 2
};
}
componentDidMount () {
document.addEventListener('mousemove', this.handleMouseMove);
}
componentWillUnmount () {
document.removeEventListener('mousemove', this.handleMouseMove);
}
handleMouseMove (event) {
this.setState({x: event.clientX, y: event.clientY});
}
render () {
if (!this.props.show) return null;
return (
<div
className={styles.mysteryMode}
style={{
backgroundPosition: `${this.state.x + 2000}px ${this.state.y + 2000}px`
}}
/>
);
}
}
MysteryMode.propTypes = {
show: PropTypes.bool
};
const mapStateToProps = state => ({
// This is the button's mode, as opposed to the actual current state
show: state.scratchGui.mysteryMode
});
export default connect(
mapStateToProps
)(MysteryMode);
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

+6 -1
View File
@@ -108,7 +108,12 @@ to adjust for the border using a different method */
left: 0;
z-index: $z-index-dragging-sprite;
filter: drop-shadow(5px 5px 5px $ui-black-transparent);
}
}
.dragging-sprite.wobbly {
/* prevent overflow (when canvas crosses the right edge of the page) from causing a scrollbar to appear */
position: fixed;
}
.stage-bottom-wrapper {
position: absolute;
+22 -7
View File
@@ -29,6 +29,8 @@ const StageComponent = props => {
onDeactivateColorPicker,
onDoubleClick,
onQuestionAnswered,
wobblyDragging,
wobblyDragRef,
...boxProps
} = props;
@@ -118,12 +120,23 @@ const StageComponent = props => {
</div>
)}
</div>
<canvas
className={styles.draggingSprite}
height={0}
ref={dragRef}
width={0}
/>
{
// Give the canvases two different keys to prevent React from recycling the same canvas
// across two different context types (2D and WebGL)
wobblyDragging ? <canvas
className={classNames(styles.draggingSprite, styles.wobbly)}
height={0}
ref={wobblyDragRef}
width={0}
key="non-wobbly"
/> : <canvas
className={styles.draggingSprite}
height={0}
ref={dragRef}
width={0}
key="wobbly"
/>
}
</Box>
{isColorPicking ? (
<Box
@@ -147,7 +160,9 @@ StageComponent.propTypes = {
onQuestionAnswered: PropTypes.func,
question: PropTypes.string,
stageSize: PropTypes.oneOf(Object.keys(STAGE_DISPLAY_SIZES)).isRequired,
useEditorDragStyle: PropTypes.bool
useEditorDragStyle: PropTypes.bool,
wobblyDragging: PropTypes.bool,
wobblyDragRef: PropTypes.func
};
StageComponent.defaultProps = {
dragRef: () => {}
+60
View File
@@ -0,0 +1,60 @@
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
/**
* Turbo Mode component passes toggleTurboMode function to its child.
* It also includes `turboMode` in the props passed to the children.
* It expects this child to be a function with the signature
* function (toggleTurboMode, {turboMode, ...props}) {}
* The component can then be used to attach turbo mode setting functionality
* to any other component:
*
* <TurboMode>{(toggleTurboMode, props) => (
* <MyCoolComponent
* turboEnabled={props.turboMode}
* onClick={toggleTurboMode}
* {...props}
* />
* )}</TurboMode>
*/
class TurboMode extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'toggleTurboMode'
]);
}
toggleTurboMode () {
this.props.vm.setTurboMode(!this.props.turboMode);
}
render () {
const {
/* eslint-disable no-unused-vars */
children,
vm,
/* eslint-enable no-unused-vars */
...props
} = this.props;
return this.props.children(this.toggleTurboMode, props);
}
}
TurboMode.propTypes = {
children: PropTypes.func,
turboMode: PropTypes.bool,
vm: PropTypes.shape({
setTurboMode: PropTypes.func
})
};
const mapStateToProps = state => ({
vm: state.scratchGui.vm,
turboMode: state.scratchGui.vmStatus.turbo
});
export default connect(
mapStateToProps,
() => ({}) // omit dispatch prop
)(TurboMode);
+55 -9
View File
@@ -10,6 +10,7 @@ import {getEventXY} from '../lib/touch-utils';
import VideoProvider from '../lib/video/video-provider';
import {SVGRenderer as V2SVGAdapter} from 'scratch-svg-renderer';
import {BitmapAdapter as V2BitmapAdapter} from 'scratch-svg-renderer';
import RubberCanvas from '../lib/rubber-canvas';
import StageComponent from '../components/stage/stage.jsx';
@@ -39,9 +40,11 @@ class Stage extends React.Component {
'updateRect',
'questionListener',
'setDragCanvas',
'setWobblyDragCanvas',
'clearDragCanvas',
'drawDragCanvas',
'positionDragCanvas'
'positionDragCanvas',
'_updateWobblyCanvas'
]);
this.state = {
mouseDownTimeoutId: null,
@@ -77,6 +80,13 @@ class Stage extends React.Component {
this.attachMouseEvents(this.canvas);
this.updateRect();
this.props.vm.runtime.addListener('QUESTION', this.questionListener);
// The GUI component seems to be recreated when entering/exiting editor mode.
// If we only update mystery mode in componentDidUpdate, it won't
// update when switching between editor and player mode.
if (this.props.mysteryMode !== this.renderer._mystery.modeActive) {
this.renderer.setMysteryMode(this.props.mysteryMode);
}
}
shouldComponentUpdate (nextProps, nextState) {
return this.props.stageSize !== nextProps.stageSize ||
@@ -85,7 +95,9 @@ class Stage extends React.Component {
this.props.isFullScreen !== nextProps.isFullScreen ||
this.state.question !== nextState.question ||
this.props.micIndicator !== nextProps.micIndicator ||
this.props.isStarted !== nextProps.isStarted;
this.props.isStarted !== nextProps.isStarted ||
this.props.mysteryMode !== nextProps.mysteryMode ||
this.props.wobblyDragging !== nextProps.wobblyDragging;
}
componentDidUpdate (prevProps) {
if (this.props.isColorPicking && !prevProps.isColorPicking) {
@@ -93,6 +105,9 @@ class Stage extends React.Component {
} else if (!this.props.isColorPicking && prevProps.isColorPicking) {
this.stopColorPickingLoop();
}
if (this.props.mysteryMode !== prevProps.mysteryMode) {
this.renderer.setMysteryMode(this.props.mysteryMode);
}
this.updateRect();
this.renderer.resize(this.rect.width, this.rect.height);
}
@@ -197,7 +212,11 @@ class Stage extends React.Component {
// Editor drag style only updates the drag canvas, does full update at the end of drag
// Non-editor drag style just updates the sprite continuously.
if (this.props.useEditorDragStyle) {
this.positionDragCanvas(mousePosition[0], mousePosition[1]);
if (this.props.wobblyDragging) {
this.wobblyDragCanvas.updateMousePosition(mousePosition);
} else {
this.positionDragCanvas(mousePosition[0], mousePosition[1]);
}
} else {
const spritePosition = this.getScratchCoords(mousePosition[0], mousePosition[1]);
this.props.vm.postSpriteInfo({
@@ -326,14 +345,19 @@ class Stage extends React.Component {
this.dragCanvas.style.display = 'block';
}
clearDragCanvas () {
this.dragCanvas.width = this.dragCanvas.height = 0;
this.dragCanvas.style.display = 'none';
const dragCanvas = this.props.wobblyDragging ? this.wobblyDragCanvas._canvas : this.dragCanvas;
dragCanvas.width = dragCanvas.height = 0;
dragCanvas.style.display = 'none';
}
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)`;
}
_updateWobblyCanvas (timestamp) {
this.wobblyDragCanvas.step(timestamp);
if (this.state.isDragging) window.requestAnimationFrame(this._updateWobblyCanvas);
}
onStartDrag (x, y) {
if (this.state.dragId) return;
const drawableId = this.renderer.pick(x, y);
@@ -349,6 +373,8 @@ class Stage extends React.Component {
// Dragging always brings the target to the front
target.goToFront();
// Ensure drag canvas' bounds are tight by forcing renderer to generate tight bounds
this.renderer.getBounds(drawableId);
// Extract the drawable art
const drawableData = this.renderer.extractDrawable(drawableId, x, y);
@@ -357,10 +383,17 @@ class Stage extends React.Component {
isDragging: true,
dragId: targetId,
dragOffset: drawableData.scratchOffset
}, () => {
if (this.props.wobblyDragging && this.props.useEditorDragStyle) this._updateWobblyCanvas(performance.now());
});
if (this.props.useEditorDragStyle) {
this.drawDragCanvas(drawableData);
this.positionDragCanvas(x, y);
if (this.props.wobblyDragging) {
this.wobblyDragCanvas.reinit(drawableData, x, y);
} else {
this.drawDragCanvas(drawableData);
this.positionDragCanvas(x, y);
}
this.props.vm.postSpriteInfo({visible: false});
}
}
@@ -400,6 +433,13 @@ class Stage extends React.Component {
setDragCanvas (canvas) {
this.dragCanvas = canvas;
}
setWobblyDragCanvas (canvas) {
if (this.wobblyDragCanvas) {
this.wobblyDragCanvas.destroy();
this.wobblyDragCanvas = null;
}
if (canvas) this.wobblyDragCanvas = new RubberCanvas(canvas);
}
render () {
const {
vm, // eslint-disable-line no-unused-vars
@@ -414,6 +454,7 @@ class Stage extends React.Component {
question={this.state.question}
onDoubleClick={this.handleDoubleClick}
onQuestionAnswered={this.handleQuestionAnswered}
wobblyDragRef={this.setWobblyDragCanvas}
{...props}
/>
);
@@ -429,7 +470,9 @@ Stage.propTypes = {
onDeactivateColorPicker: PropTypes.func,
stageSize: PropTypes.oneOf(Object.keys(STAGE_DISPLAY_SIZES)).isRequired,
useEditorDragStyle: PropTypes.bool,
vm: PropTypes.instanceOf(VM).isRequired
vm: PropTypes.instanceOf(VM).isRequired,
mysteryMode: PropTypes.bool,
wobblyDragging: PropTypes.bool
};
Stage.defaultProps = {
@@ -442,7 +485,10 @@ const mapStateToProps = state => ({
isStarted: state.scratchGui.vmStatus.started,
micIndicator: state.scratchGui.micIndicator,
// Do not use editor drag style in fullscreen or player mode.
useEditorDragStyle: !(state.scratchGui.mode.isFullScreen || state.scratchGui.mode.isPlayerOnly)
useEditorDragStyle: !(state.scratchGui.mode.isFullScreen || state.scratchGui.mode.isPlayerOnly),
mysteryMode: state.scratchGui.mysteryMode && !(
state.scratchGui.mode.isFullScreen || state.scratchGui.mode.isPlayerOnly),
wobblyDragging: state.scratchGui.wobblyDragging
});
const mapDispatchToProps = dispatch => ({
+18
View File
@@ -51,6 +51,9 @@ import libraryCodeCartoon from './thumbnails/code-a-cartoon.jpg';
// Talking Tales
import libraryTalking from './thumbnails/talking.png';
// Sprite Mystery
import librarySpriteMystery from './thumbnails/sprite-mystery.png';
// Videos
import recordASound from './thumbnails/record-a-sound.jpg';
import glideAroundThumb from './thumbnails/glide-around.jpg';
@@ -497,6 +500,21 @@ export default {
urlId: 'animations-that-talk'
},
'sprite-mystery': {
name: 'How to Find a Sprite',
img: librarySpriteMystery,
steps: [{
video: 'sprite-mystery'
}, {
deckIds: [
'intro-move-sayhello',
'add-sprite'
]
}
],
urlId: 'sprite-mystery'
},
'talking': {
name: (
<FormattedMessage
Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

@@ -129,6 +129,9 @@ const videos = {
},
'talking': {
en: 'j0208mq4qi'
},
'sprite-mystery': {
en: 's69h0y6cs4'
}
};
+8
View File
@@ -0,0 +1,8 @@
precision mediump float;
uniform sampler2D u_image0;
varying vec2 v_texCoord;
void main () {
gl_FragColor = texture2D(u_image0, v_texCoord);
}
+15
View File
@@ -0,0 +1,15 @@
precision mediump float;
uniform vec4 u_bounds;
attribute vec2 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main () {
vec2 boundsSize = u_bounds.zw - u_bounds.xy;
vec2 adjusted = a_position - vec2(u_bounds.x, u_bounds.y);
vec4 pos = vec4(((adjusted / boundsSize) - 0.5) * vec2(2.0, -2.0), 0.0, 1.0);
v_texCoord = a_texCoord;
gl_Position = pos;
}
+358
View File
@@ -0,0 +1,358 @@
import VERTEX_SOURCE from 'raw-loader!./rubber-canvas-vert.glsl';
import FRAGMENT_SOURCE from 'raw-loader!./rubber-canvas-frag.glsl';
const DAMPING = 20;
class _Spring {
constructor (opts) {
this.reinit(opts);
}
reinit (opts) {
if (Object.prototype.hasOwnProperty.call(opts, 'stiffness')) this.stiffness = opts.stiffness;
if (Object.prototype.hasOwnProperty.call(opts, 'mass')) this.mass = opts.mass;
this.anchor = opts.anchor;
this.position = opts.anchor.slice(0);
this._velocity = [0, 0];
}
step (deltaTime) {
const springForceX = -this.stiffness * (this.position[0] - this.anchor[0]);
const springForceY = -this.stiffness * (this.position[1] - this.anchor[1]);
const dampingForceX = (DAMPING * this.mass) * this._velocity[0];
const dampingForceY = (DAMPING * this.mass) * this._velocity[1];
const forceX = springForceX - dampingForceX;
const forceY = springForceY - dampingForceY;
const accelerationX = forceX / this.mass;
const accelerationY = forceY / this.mass;
this._velocity[0] += accelerationX * deltaTime;
this._velocity[1] += accelerationY * deltaTime;
this.position[0] += this._velocity[0] * deltaTime;
this.position[1] += this._velocity[1] * deltaTime;
}
}
const createShader = (gl, source, type) => {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const info = gl.getShaderInfoLog(shader);
throw new Error(`Could not compile WebGL program. \n${info}`);
}
return shader;
};
const createProgram = (gl, vertSource, fragSource) => {
const vertShader = createShader(gl, vertSource, gl.VERTEX_SHADER);
const fragShader = createShader(gl, fragSource, gl.FRAGMENT_SHADER);
const program = gl.createProgram();
gl.attachShader(program, vertShader);
gl.attachShader(program, fragShader);
gl.linkProgram(program);
const programInfo = {
uniforms: {},
attribs: {},
program
};
// Construct maps of uniform + attrib locations for convenience
const numActiveUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
for (let i = 0; i < numActiveUniforms; i++) {
const uniformInfo = gl.getActiveUniform(program, i);
programInfo.uniforms[uniformInfo.name] = gl.getUniformLocation(program, uniformInfo.name);
}
const numActiveAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
for (let i = 0; i < numActiveAttributes; i++) {
const attribInfo = gl.getActiveAttrib(program, i);
programInfo.attribs[attribInfo.name] = gl.getAttribLocation(program, attribInfo.name);
}
return programInfo;
};
// Resolution of the grid in terms of grid squares. In this case, that's 16 squares and 25 vertices.
const SUBDIVISIONS = 4;
class RubberCanvas {
constructor (canvas) {
this._canvas = canvas;
this._gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
this._size = [0, 0];
this._mousePosition = [0, 0];
const numPoints = (SUBDIVISIONS + 1) ** 2;
this._springs = [];
for (let i = 0; i < numPoints; i++) {
this._springs.push(new _Spring({
anchor: [0, 0],
position: [0, 0]
}));
}
this._vertexData = new Float32Array(numPoints * 2);
this._texCoordData = new Float32Array(numPoints * 2);
this._vertexIndices = new Uint16Array(numPoints * 6);
// Initialize texture coordinates for all grid points
for (let x = 0; x <= SUBDIVISIONS; x++) {
for (let y = 0; y <= SUBDIVISIONS; y++) {
const index = (y * (SUBDIVISIONS + 1)) + x;
this._texCoordData[index * 2] = x / SUBDIVISIONS;
this._texCoordData[(index * 2) + 1] = y / SUBDIVISIONS;
}
}
// Initialize vertex indices
for (let x = 0; x < SUBDIVISIONS; x++) {
for (let y = 0; y < SUBDIVISIONS; y++) {
const topLeftIndex = (y * (SUBDIVISIONS + 1)) + x;
const topRightIndex = (y * (SUBDIVISIONS + 1)) + x + 1;
const bottomLeftIndex = ((y + 1) * (SUBDIVISIONS + 1)) + x;
const bottomRightIndex = ((y + 1) * (SUBDIVISIONS + 1)) + x + 1;
this._vertexIndices[topLeftIndex * 6] = topLeftIndex;
this._vertexIndices[(topLeftIndex * 6) + 1] = topRightIndex;
this._vertexIndices[(topLeftIndex * 6) + 2] = bottomLeftIndex;
this._vertexIndices[(topLeftIndex * 6) + 3] = bottomLeftIndex;
this._vertexIndices[(topLeftIndex * 6) + 4] = topRightIndex;
this._vertexIndices[(topLeftIndex * 6) + 5] = bottomRightIndex;
}
}
// Used for delta-time calculation
this._lastTimestamp = 0;
const gl = this._gl;
this._shader = createProgram(gl, VERTEX_SOURCE, FRAGMENT_SOURCE);
this._vertexBuffer = gl.createBuffer();
this._indexBuffer = gl.createBuffer();
this._texCoordBuffer = gl.createBuffer();
gl.enableVertexAttribArray(this._shader.attribs.a_position);
gl.enableVertexAttribArray(this._shader.attribs.a_texCoord);
// The texture coordinates and vertex indices will never change, so send them over to the GPU here
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._indexBuffer);
gl.bufferData(
gl.ELEMENT_ARRAY_BUFFER,
this._vertexIndices,
gl.STATIC_DRAW
);
gl.bindBuffer(gl.ARRAY_BUFFER, this._texCoordBuffer);
gl.vertexAttribPointer(
this._shader.attribs.a_texCoord,
2, // vec2
gl.FLOAT,
false,
0,
0
);
gl.bufferData(
gl.ARRAY_BUFFER,
this._texCoordData,
gl.STATIC_DRAW
);
gl.activeTexture(gl.TEXTURE0);
this._texture = gl.createTexture();
// This will be correct if https://github.com/LLK/scratch-render/pull/556 goes in in time
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
gl.clearColor(0, 0, 0, 0);
}
reinit (drawableData, x, y) {
const {
width,
height,
x: grabX,
y: grabY
} = drawableData;
this._setData(drawableData);
// Reinitialize springs' mass based on their distance to the point at which the sprite was grabbed
for (let i = 0; i <= SUBDIVISIONS; i++) {
const springX = (i / SUBDIVISIONS) * width;
for (let j = 0; j <= SUBDIVISIONS; j++) {
const springY = (j / SUBDIVISIONS) * height;
const distance = Math.hypot(springX - grabX, springY - grabY);
const distanceScaled = distance / Math.hypot(width, height);
this._springs[(j * (SUBDIVISIONS + 1)) + i].reinit({
anchor: [springX + x, springY + y],
position: [springX + x, springY + y],
stiffness: 15,
mass: (distanceScaled * 0.05) + 0.025
});
}
}
this._lastTimestamp = performance.now();
const {x: parentX, y: parentY} = this._canvas.parentElement.getBoundingClientRect();
this._canvas.style.left = `${-grabX + parentX}px`;
this._canvas.style.top = `${-grabY + parentY}px`;
this._canvas.style.display = 'block';
this.step();
}
// Calculate the bounds of the dragged sprite after being jellofied.
_calculateBounds () {
let left = Infinity;
let right = -Infinity;
let top = Infinity;
let bottom = -Infinity;
for (const spring of this._springs) {
left = Math.min(left, spring.position[0]);
right = Math.max(right, spring.position[0]);
top = Math.min(top, spring.position[1]);
bottom = Math.max(bottom, spring.position[1]);
}
left = Math.floor(left);
right = Math.ceil(right);
bottom = Math.ceil(bottom);
top = Math.floor(top);
return {left, right, top, bottom};
}
_setData (data) {
this._size[0] = data.width;
this._size[1] = data.height;
const gl = this._gl;
gl.bindTexture(gl.TEXTURE_2D, this._texture);
// disable automatic mipmapping
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, data.width, data.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data.data);
}
updateMousePosition ([x, y]) {
this._mousePosition[0] = x;
this._mousePosition[1] = y;
// update springs' destinations to match new mouse position
for (let i = 0; i <= SUBDIVISIONS; i++) {
const springX = (i / SUBDIVISIONS) * this._size[0];
for (let j = 0; j <= SUBDIVISIONS; j++) {
const springY = (j / SUBDIVISIONS) * this._size[1];
this._springs[(j * (SUBDIVISIONS + 1)) + i].anchor[0] = springX + x;
this._springs[(j * (SUBDIVISIONS + 1)) + i].anchor[1] = springY + y;
}
}
}
step (timestamp) {
// Slightly above 33.3ms (30 FPS)
const maxTimeStep = 35;
let deltaTime = timestamp - this._lastTimestamp;
// Take multiple steps if deltaTime is too large
// This prevents explodage
let timeSubdivisions = 1;
if (deltaTime > maxTimeStep) {
timeSubdivisions = Math.ceil(deltaTime / maxTimeStep);
deltaTime /= timeSubdivisions;
}
this._lastTimestamp = timestamp;
if (deltaTime > 0) {
for (let i = 0; i < timeSubdivisions; i++) {
for (const spring of this._springs) {
// I've got a step() in my _Spring
spring.step(deltaTime / 1000);
}
}
}
// Update vertex data to match springs' positions
for (let i = 0; i < this._springs.length; i++) {
const spring = this._springs[i];
this._vertexData[i * 2] = spring.position[0];
this._vertexData[(i * 2) + 1] = spring.position[1];
}
const gl = this._gl;
gl.bindBuffer(gl.ARRAY_BUFFER, this._vertexBuffer);
gl.vertexAttribPointer(
this._shader.attribs.a_position,
2, // vec2
gl.FLOAT,
false,
0,
0
);
gl.bufferData(
gl.ARRAY_BUFFER,
this._vertexData,
gl.DYNAMIC_DRAW
);
const bounds = this._calculateBounds();
// 10 pixels of slack space to avoid excessive canvas resizing
bounds.right += 10;
bounds.bottom += 10;
// Only scale the canvas up, not down-- this also avoids resizing the canvas too much
if (bounds.right - bounds.left < this._canvas.width) bounds.right = bounds.left + this._canvas.width;
if (bounds.bottom - bounds.top < this._canvas.height) bounds.bottom = bounds.top + this._canvas.height;
const width = bounds.right - bounds.left;
const height = bounds.bottom - bounds.top;
if (width > 2048 || height > 2048) {
// Avoid creating lag spikes from a huge WebGL canvas if the spring simulation blows up
return;
}
this._canvas.width = width;
this._canvas.height = height;
gl.useProgram(this._shader.program);
gl.uniform1i(this._shader.uniforms.u_image0, 0);
gl.uniform4f(this._shader.uniforms.u_bounds, bounds.left, bounds.top, bounds.right, bounds.bottom);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, this._texture);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.viewport(0, 0, width, height);
gl.drawElements(gl.TRIANGLES, this._vertexIndices.length, gl.UNSIGNED_SHORT, 0);
this._canvas.style.transform = `translate(${bounds.left}px, ${bounds.top}px)`;
}
destroy () {
const gl = this._gl;
gl.deleteBuffer(this._texCoordBuffer);
gl.deleteBuffer(this._vertexBuffer);
gl.deleteBuffer(this._indexBuffer);
gl.deleteTexture(this._texture);
gl.deleteProgram(this._shader.program);
}
}
export default RubberCanvas;
+8 -2
View File
@@ -14,6 +14,8 @@ import modalReducer, {modalsInitialState} from './modals';
import modeReducer, {modeInitialState} from './mode';
import monitorReducer, {monitorsInitialState} from './monitors';
import monitorLayoutReducer, {monitorLayoutInitialState} from './monitor-layout';
import mysteryModeReducer, {mysteryModeInitialState} from './mystery-mode';
import wobblyDragReducer, {wobblyDragInitialState} from './wobbly-dragging';
import projectChangedReducer, {projectChangedInitialState} from './project-changed';
import projectStateReducer, {projectStateInitialState} from './project-state';
import projectTitleReducer, {projectTitleInitialState} from './project-title';
@@ -48,6 +50,7 @@ const guiInitialState = {
modals: modalsInitialState,
monitors: monitorsInitialState,
monitorLayout: monitorLayoutInitialState,
mysteryMode: mysteryModeInitialState,
projectChanged: projectChangedInitialState,
projectState: projectStateInitialState,
projectTitle: projectTitleInitialState,
@@ -57,7 +60,8 @@ const guiInitialState = {
timeout: timeoutInitialState,
toolbox: toolboxInitialState,
vm: vmInitialState,
vmStatus: vmStatusInitialState
vmStatus: vmStatusInitialState,
wobblyDragging: wobblyDragInitialState
};
const initPlayer = function (currentState) {
@@ -146,6 +150,7 @@ const guiReducer = combineReducers({
modals: modalReducer,
monitors: monitorReducer,
monitorLayout: monitorLayoutReducer,
mysteryMode: mysteryModeReducer,
projectChanged: projectChangedReducer,
projectState: projectStateReducer,
projectTitle: projectTitleReducer,
@@ -155,7 +160,8 @@ const guiReducer = combineReducers({
timeout: timeoutReducer,
toolbox: toolboxReducer,
vm: vmReducer,
vmStatus: vmStatusReducer
vmStatus: vmStatusReducer,
wobblyDragging: wobblyDragReducer
});
export {
+26
View File
@@ -0,0 +1,26 @@
const TOGGLE_MYSTERY_MODE = 'scratch-gui/mystery-mode/TOGGLE_MYSTERY_MODE';
const initialState = false;
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case TOGGLE_MYSTERY_MODE:
return !state;
default:
return state;
}
};
const toggleMysteryMode = function () {
return {
type: TOGGLE_MYSTERY_MODE
};
};
export {
reducer as default,
initialState as mysteryModeInitialState,
toggleMysteryMode
};
+26
View File
@@ -0,0 +1,26 @@
const TOGGLE_WOBBLE = 'scratch-gui/mystery-mode/TOGGLE_WOBBLE';
const initialState = false;
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case TOGGLE_WOBBLE:
return !state;
default:
return state;
}
};
const toggleWobblyDragging = function () {
return {
type: TOGGLE_WOBBLE
};
};
export {
reducer as default,
initialState as wobblyDragInitialState,
toggleWobblyDragging
};