diff --git a/.gitattributes b/.gitattributes index 84e89375a..e81c38bb5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,6 +6,11 @@ # File types which we know are binary +# Treat SVG files as binary so that their contents don't change due to line +# endings. The contents of SVGs must not change from the way they're stored +# on assets.scratch.mit.edu so that MD5 calculations don't change. +*.svg binary + # Prefer LF for most file types *.frag text eol=lf *.htm text eol=lf diff --git a/.gitignore b/.gitignore index 0b8a0db40..de7972e5e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ # NPM /node_modules npm-* +/package-lock.json # Testing /.nyc_output diff --git a/.travis.yml b/.travis.yml index ba21af324..9290080d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,6 +35,7 @@ deploy: - master - develop - smoke + condition: $TRAVIS_EVENT_TYPE != cron skip_cleanup: true email: $NPM_EMAIL api_key: $NPM_TOKEN @@ -44,6 +45,7 @@ deploy: branch: - master - $PREVIEW_BRANCH + condition: $TRAVIS_EVENT_TYPE != cron access_key_id: $AWS_ACCESS_KEY_ID secret_access_key: $AWS_SECRET_ACCESS_KEY bucket: $AWS_BUCKET_NAME @@ -53,11 +55,19 @@ deploy: - provider: script on: all_branches: true + condition: $TRAVIS_EVENT_TYPE != cron skip_cleanup: true script: npm run deploy -- -x -e $TRAVIS_BRANCH -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git - provider: script on: all_branches: true + condition: $TRAVIS_EVENT_TYPE != cron script: npm run prune -- https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git +- provider: script + on: + branch: develop + condition: $TRAVIS_EVENT_TYPE == cron + skip_cleanup: true + script: npm run i18n:src && npm run i18n:push after_deploy: - 'curl -X POST -H "Fastly-Key: $FASTLY_TOKEN" -H "Accept: application/json" https://api.fastly.com/service/$FASTLY_SERVICE_ID/purge_all' diff --git a/README.md b/README.md index 1f49dca4f..440a3a4b0 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ npm install https://github.com/LLK/scratch-gui.git ``` If you want to edit/play yourself: ```bash -git clone git@github.com:LLK/scratch-gui.git +git clone https://github.com/LLK/scratch-gui.git cd scratch-gui npm install ``` diff --git a/package.json b/package.json index db6278c23..65ebd0c7b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "clean": "rimraf ./build && mkdirp build && rimraf ./dist && mkdirp dist", "deploy": "touch build/.nojekyll && gh-pages -t -d build -m \"Build for $(git log --pretty=format:%H -n1)\"", "prune": "./prune-gh-pages.sh", - "i18n:src": "babel src > tmp.js && rimraf tmp.js && build-i18n-src ./translations/messages/src ./translations/", + "i18n:push": "tx-push-src scratch-editor interface translations/en.json", + "i18n:src": "rimraf ./translations/messages/src && babel src > tmp.js && rimraf tmp.js && build-i18n-src ./translations/messages/src ./translations/ && npm run i18n:push", "start": "webpack-dev-server", "test": "npm run test:lint && npm run test:unit && npm run build && npm run test:integration", "test:integration": "jest --runInBand test[\\\\/]integration", @@ -43,7 +44,7 @@ "babel-loader": "^8.0.4", "base64-loader": "1.0.0", "bowser": "1.9.4", - "chromedriver": "2.43.1", + "chromedriver": "2.44.1", "classnames": "2.2.6", "computed-style-to-inline-style": "3.0.0", "copy-webpack-plugin": "^4.5.1", @@ -55,7 +56,7 @@ "eslint": "^5.0.1", "eslint-config-scratch": "^5.0.0", "eslint-plugin-import": "^2.8.0", - "eslint-plugin-react": "^7.5.1", + "eslint-plugin-react": "7.11.1", "file-loader": "2.0.0", "get-float-time-domain-data": "0.1.0", "get-user-media-promise": "1.1.4", @@ -72,8 +73,10 @@ "lodash.isequal": "4.5.0", "lodash.omit": "4.5.0", "lodash.pick": "4.4.0", + "lodash.throttle": "4.0.1", "minilog": "3.1.0", "mkdirp": "^0.5.1", + "papaparse": "4.6.2", "postcss-import": "^12.0.0", "postcss-loader": "^3.0.0", "postcss-simple-vars": "^5.0.1", @@ -100,14 +103,14 @@ "redux-mock-store": "^1.2.3", "redux-throttle": "0.1.1", "rimraf": "^2.6.1", - "scratch-audio": "0.1.0-prerelease.20181023202904", - "scratch-blocks": "0.1.0-prerelease.1541684350", - "scratch-l10n": "3.0.20181108141849", - "scratch-paint": "0.2.0-prerelease.20181109181214", - "scratch-render": "0.1.0-prerelease.20181102130522", - "scratch-storage": "1.2.0", - "scratch-svg-renderer": "0.2.0-prerelease.20181101210634", - "scratch-vm": "0.2.0-prerelease.20181108103702", + "scratch-audio": "0.1.0-prerelease.20190114210212", + "scratch-blocks": "0.1.0-prerelease.1548885087", + "scratch-l10n": "3.1.20190130142816", + "scratch-paint": "0.2.0-prerelease.20190114205252", + "scratch-render": "0.1.0-prerelease.20190128154859", + "scratch-storage": "1.2.2", + "scratch-svg-renderer": "0.2.0-prerelease.20190125192231", + "scratch-vm": "0.2.0-prerelease.20190130160436", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", "style-loader": "^0.23.0", diff --git a/src/components/action-menu/action-menu.css b/src/components/action-menu/action-menu.css index e82103ca1..14dbb5df0 100644 --- a/src/components/action-menu/action-menu.css +++ b/src/components/action-menu/action-menu.css @@ -1,4 +1,5 @@ @import "../../css/colors.css"; +@import "../../css/units.css"; @import "../../css/z-index.css"; $main-button-size: 2.75rem; @@ -153,7 +154,7 @@ button::-moz-focus-inner { .tooltip { border: 1px solid hsla(0, 0%, 0%, .1) !important; - border-radius: .25rem !important; + border-radius: $form-radius !important; box-shadow: 0 0 .5rem hsla(0, 0%, 0%, .25) !important; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; z-index: $z-index-tooltip !important; diff --git a/src/components/action-menu/action-menu.jsx b/src/components/action-menu/action-menu.jsx index 01070b27f..36cf5d444 100644 --- a/src/components/action-menu/action-menu.jsx +++ b/src/components/action-menu/action-menu.jsx @@ -78,6 +78,9 @@ class ActionMenu extends React.Component { return event => { ReactTooltip.hide(); if (fn) fn(event); + // Blur the button so it does not keep focus after being clicked + // This prevents keyboard events from triggering the button + this.buttonRef.blur(); this.setState({forceHide: true, isOpen: false}, () => { setTimeout(() => this.setState({forceHide: false})); }); diff --git a/src/components/alerts/alert.css b/src/components/alerts/alert.css index 0787a9fb1..8e5d56296 100644 --- a/src/components/alerts/alert.css +++ b/src/components/alerts/alert.css @@ -4,29 +4,59 @@ .alert { width: 100%; - background: #FFF0DF; display: flex; flex-direction: row; overflow: hidden; - align-items: left; - border: 1px solid #FF8C1A; - border-radius: 8px; - padding: 8px; - box-shadow: 0px 0px 0px 2px rgba(255, 140, 26, 0.25); + justify-content: flex-start; + border-radius: $space; + padding-top: .875rem; + padding-bottom: .875rem; + padding-left: 1rem; + padding-right: 1rem; margin-bottom: 7px; + min-height: 1.5rem; +} + +.alert.warn { + background: #FFF0DF; + border: 1px solid #FF8C1A; + box-shadow: 0px 0px 0px 2px rgba(255, 140, 26, 0.25); +} + +.alert.success { + background: $extensions-light; + border: 1px solid $extensions-tertiary; + box-shadow: 0px 0px 0px 2px $extensions-light; +} + +.alert-spinner { + self-align: center; +} + +.icon-section { + min-width: 1.25rem; + min-height: 1.25rem; + display: flex; + padding-right: 1rem; } .alert-icon { - margin-right: 5px; vertical-align: middle; } .alert-message { color: #555; font-weight: bold; - font-size: 12px; - line-height: 22pt; - width: 100%; + font-size: .8125rem; + line-height: .875rem; + display: flex; + align-items: center; + padding-right: .5rem; +} + +.alert-buttons { + display: flex; + flex-direction: row; } .alert-close-button { @@ -34,25 +64,40 @@ } .alert-close-button-container { - margin-top: 7px; - margin-right: 4px; - outline-style:none; + outline-style: none; width: 30px; height: 30px; + align-self: center; } -.connection-button { +.alert-connection-button { + min-height: 2rem; + width: 6.5rem; padding: 0.55rem 0.9rem; border-radius: 0.35rem; background: #FF8C1A; color: white; font-weight: 700; font-size: 0.77rem; - margin: 0.25rem; border: none; cursor: pointer; display: flex; + justify-content: center; align-items: center; - margin-right: 13px; + align-self: stretch; outline-style:none; } + +[dir="ltr"] .alert-connection-button { + margin-right: 13px; +} + +[dir="rtl"] .alert-connection-button { + margin-left: 13px; +} + +/* prevent last button in list from too much margin to edge of alert */ +.alert-buttons > :last-child { + margin-left: 0; + margin-right: 0; +} diff --git a/src/components/alerts/alert.jsx b/src/components/alerts/alert.jsx index 2ec848984..db86bd4b8 100644 --- a/src/components/alerts/alert.jsx +++ b/src/components/alerts/alert.jsx @@ -1,62 +1,140 @@ import React from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import {FormattedMessage} from 'react-intl'; import Box from '../box/box.jsx'; import CloseButton from '../close-button/close-button.jsx'; +import Spinner from '../spinner/spinner.jsx'; +import {AlertLevels} from '../../lib/alerts/index.jsx'; import styles from './alert.css'; +const closeButtonColors = { + [AlertLevels.SUCCESS]: CloseButton.COLOR_GREEN, + [AlertLevels.WARN]: CloseButton.COLOR_ORANGE +}; + const AlertComponent = ({ + content, + closeButton, + extensionName, + iconSpinner, iconURL, - message, + level, + showDownload, + showSaveNow, onCloseAlert, + onDownload, + onSaveNow, onReconnect, showReconnect }) => ( + {/* TODO: implement Rtl handling */} + {(iconSpinner || iconURL) && ( +
+ {iconSpinner && ( + + )} + {iconURL && ( + + )} +
+ )}
- {iconURL ? ( - - ) : null} - {message} -
- {showReconnect ? ( - - ) : null} - - - + ) : content} + +
+ {showSaveNow && ( + + )} + {showDownload && ( + + )} + {showReconnect && ( + + )} + {closeButton && ( + + + + )} +
); AlertComponent.propTypes = { + closeButton: PropTypes.bool, + content: PropTypes.oneOfType([PropTypes.element, PropTypes.string]), + extensionName: PropTypes.string, + iconSpinner: PropTypes.bool, iconURL: PropTypes.string, - message: PropTypes.string, + level: PropTypes.string, onCloseAlert: PropTypes.func.isRequired, + onDownload: PropTypes.func, onReconnect: PropTypes.func, - showReconnect: PropTypes.bool.isRequired + onSaveNow: PropTypes.func, + showDownload: PropTypes.func, + showReconnect: PropTypes.bool, + showSaveNow: PropTypes.bool +}; + +AlertComponent.defaultProps = { + level: AlertLevels.WARN }; export default AlertComponent; diff --git a/src/components/alerts/alerts.css b/src/components/alerts/alerts.css new file mode 100644 index 000000000..3b7cf5221 --- /dev/null +++ b/src/components/alerts/alerts.css @@ -0,0 +1,4 @@ +.alerts-inner-container { + min-width: 200px; + max-width: 548px; +} diff --git a/src/components/alerts/alerts.jsx b/src/components/alerts/alerts.jsx index 9a87f766c..50acd7ce1 100644 --- a/src/components/alerts/alerts.jsx +++ b/src/components/alerts/alerts.jsx @@ -4,6 +4,8 @@ import PropTypes from 'prop-types'; import Box from '../box/box.jsx'; import Alert from '../../containers/alert.jsx'; +import styles from './alerts.css'; + const AlertsComponent = ({ alertsList, className, @@ -13,17 +15,26 @@ const AlertsComponent = ({ bounds="parent" className={className} > - {alertsList.map((a, index) => ( - - ))} + + {alertsList.map((a, index) => ( + + ))} + ); diff --git a/src/components/alerts/inline-message.css b/src/components/alerts/inline-message.css new file mode 100644 index 000000000..b5169b41e --- /dev/null +++ b/src/components/alerts/inline-message.css @@ -0,0 +1,27 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; + +.inline-message { + color: $ui-white; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + display: flex; + justify-content: end; + align-items: center; + font-size: .8125rem; +} + +.success { + color: $ui-white-dim; +} + +.info { + color: $ui-white; +} + +.warn { + color: $error-light; +} + +.spinner { + margin-right: $space; +} diff --git a/src/components/alerts/inline-message.jsx b/src/components/alerts/inline-message.jsx new file mode 100644 index 000000000..981e1984a --- /dev/null +++ b/src/components/alerts/inline-message.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import Spinner from '../spinner/spinner.jsx'; +import {AlertLevels} from '../../lib/alerts/index.jsx'; + +import styles from './inline-message.css'; + +const InlineMessageComponent = ({ + content, + iconSpinner, + level +}) => ( +
+ {/* TODO: implement Rtl handling */} + {iconSpinner && ( + + )} + {content} +
+); + +InlineMessageComponent.propTypes = { + content: PropTypes.element, + iconSpinner: PropTypes.bool, + level: PropTypes.string +}; + +InlineMessageComponent.defaultProps = { + level: AlertLevels.INFO +}; + +export default InlineMessageComponent; diff --git a/src/components/asset-panel/selector.css b/src/components/asset-panel/selector.css index 535c38b0b..611d901b1 100644 --- a/src/components/asset-panel/selector.css +++ b/src/components/asset-panel/selector.css @@ -56,7 +56,7 @@ $fade-out-distance: 100px; .list-item { width: 5rem; - min-height: 5rem; + height: 5rem; margin: 0.5rem auto; } @@ -72,6 +72,6 @@ $fade-out-distance: 100px; .list-item.placeholder { - background: black; + background: white; filter: opacity(15%) brightness(0%); } diff --git a/src/components/audio-trimmer/audio-trimmer.css b/src/components/audio-trimmer/audio-trimmer.css index fe276c381..63af2a0cb 100644 --- a/src/components/audio-trimmer/audio-trimmer.css +++ b/src/components/audio-trimmer/audio-trimmer.css @@ -12,6 +12,9 @@ $hover-scale: 2; left: 0; width: 100%; height: 100%; + + /* Force the browser to paint separately to avoid composite cost with waveform */ + transform: translateZ(0); } .trim-background { @@ -50,8 +53,25 @@ $hover-scale: 2; border: 1px solid $red-tertiary; } +.playhead-container { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + overflow: hidden; +} + .playhead { - border: 1px solid $motion-primary; + /* + Even though playhead is just a line, it is 100% width (the width of the waveform) + so that we can use transform: translateX() using percentages. + */ + width: 100%; + border-left: 1px solid $motion-primary; + border-top: none; + border-bottom: none; + border-right: none; } .start-trim-line { diff --git a/src/components/audio-trimmer/audio-trimmer.jsx b/src/components/audio-trimmer/audio-trimmer.jsx index d1b21c926..87ca75a98 100644 --- a/src/components/audio-trimmer/audio-trimmer.jsx +++ b/src/components/audio-trimmer/audio-trimmer.jsx @@ -32,12 +32,14 @@ const AudioTrimmer = props => ( )} {props.playhead ? ( - +
+
+
) : null} {props.trimEnd === null ? null : ( diff --git a/src/components/backpack/backpack.css b/src/components/backpack/backpack.css index 6eddfa905..25bc42d45 100644 --- a/src/components/backpack/backpack.css +++ b/src/components/backpack/backpack.css @@ -57,7 +57,7 @@ width: 100%; height: 100%; opacity: 0.75; - background-color: #8cbcff; + background-color: $drop-highlight; transition: all 0.25s ease; } @@ -69,8 +69,18 @@ } .backpack-item { - min-width: 4rem; + width: 4rem; + height: 4.5rem; margin: 0 0.25rem; + flex-shrink: 0; + + /* Need to hide overflow because of background setting below */ + overflow: hidden; +} + +.backpack-item > div { + /* Need to set the background to get blend-mode below to work */ + background: $ui-primary; } .backpack-item img { diff --git a/src/components/blocks/blocks.css b/src/components/blocks/blocks.css index e0b07ce26..fd66c68bd 100644 --- a/src/components/blocks/blocks.css +++ b/src/components/blocks/blocks.css @@ -13,7 +13,7 @@ width: 100%; height: 100%; opacity: 0.75; - background-color: #8cbcff; + background-color: $drop-highlight; transition: all 0.25s ease; } diff --git a/src/components/browser-modal/browser-modal.css b/src/components/browser-modal/browser-modal.css index a7f86ac4e..2f4145bf6 100644 --- a/src/components/browser-modal/browser-modal.css +++ b/src/components/browser-modal/browser-modal.css @@ -27,17 +27,23 @@ } .illustration { + display: flex; + align-items: center; + justify-content: center; width: 100%; - height: 208px; - background-color: $motion-primary; - background-image: url('./unsupported.png'); - background-size: cover; + height: 100px; + background-color: $control-primary; } [dir="rtl"] .illustration { transform: scaleX(-1); } +.illustration img { + height: 80%; + width: auto; +} + .body { background: $ui-white; padding: 1.5rem 2.25rem; diff --git a/src/components/browser-modal/browser-modal.jsx b/src/components/browser-modal/browser-modal.jsx index 9afac0e38..0419e9122 100644 --- a/src/components/browser-modal/browser-modal.jsx +++ b/src/components/browser-modal/browser-modal.jsx @@ -5,85 +5,107 @@ import Box from '../box/box.jsx'; import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl'; import styles from './browser-modal.css'; +import unhappyBrowser from './unsupported-browser.svg'; const messages = defineMessages({ label: { id: 'gui.unsupportedBrowser.label', defaultMessage: 'Browser is not supported', description: '' + }, + error: { + id: 'gui.unsupportedBrowser.errorLabel', + defaultMessage: 'An Error Occurred', + description: 'Heading shown when there is an unhandled exception in an unsupported browser' } }); -const BrowserModal = ({intl, ...props}) => ( - -
- - - -

- -

-

- { /* eslint-disable max-len */ } - - { /* eslint-enable max-len */ } -

- - - - +const BrowserModal = ({intl, ...props}) => { + const label = props.error ? messages.error : messages.label; + return ( + +
+ + -
- - - - ) - }} - /> -
- -
-
-); + + +

+ +

+

+ { /* eslint-disable max-len */ } + { + props.error ? : + } + { /* eslint-enable max-len */ } +

+ + + + + +
+ + + + ) + }} + /> +
+
+
+
+ ); +}; BrowserModal.propTypes = { + error: PropTypes.bool, intl: intlShape.isRequired, isRtl: PropTypes.bool, onBack: PropTypes.func.isRequired }; +BrowserModal.defaultProps = { + error: false +}; + const WrappedBrowserModal = injectIntl(BrowserModal); WrappedBrowserModal.setAppElement = ReactModal.setAppElement; diff --git a/src/components/browser-modal/unsupported-browser.svg b/src/components/browser-modal/unsupported-browser.svg new file mode 100644 index 000000000..7749f7539 --- /dev/null +++ b/src/components/browser-modal/unsupported-browser.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/button/button.css b/src/components/button/button.css index 9ba2fbd93..30f000a7a 100644 --- a/src/components/button/button.css +++ b/src/components/button/button.css @@ -1,6 +1,8 @@ +@import "../../css/units.css"; + .outlined-button { cursor: pointer; - border-radius: .25rem; + border-radius: $form-radius; font-weight: bold; display: flex; flex-direction: row; diff --git a/src/components/cards/cards.jsx b/src/components/cards/cards.jsx index 129d2ca57..f137c91a0 100644 --- a/src/components/cards/cards.jsx +++ b/src/components/cards/cards.jsx @@ -234,7 +234,11 @@ const Cards = props => { if (x === 0 && y === 0) { // initialize positions x = isRtl ? -292 : 292; - y = 365; + // The tallest cards are about 385px high, and the default position is pinned + // to near the bottom of the blocks palette to allow room to work above. + const tallCardHeight = 385; + const bottomMargin = 60; // To avoid overlapping the backpack region + y = window.innerHeight - tallCardHeight - bottomMargin; } const steps = content[activeDeckId].steps; diff --git a/src/components/close-button/close-button.css b/src/components/close-button/close-button.css index 90bd473ce..48a18f88d 100644 --- a/src/components/close-button/close-button.css +++ b/src/components/close-button/close-button.css @@ -28,8 +28,8 @@ .small { width: 0.825rem; height: 0.825rem; - color: $ui-white; background-color: $motion-primary; + color: $ui-white; } .large { @@ -38,7 +38,7 @@ box-shadow: 0 0 0 2px $ui-black-transparent; } -.large.orange { + .large.orange { background-color: hsla(29, 100%, 54%, 0.2); box-shadow: 0px 0px 0px 2px hsla(29, 100%, 54%, 0.2); } diff --git a/src/components/close-button/close-button.jsx b/src/components/close-button/close-button.jsx index b41a3d97c..1bfe36d69 100644 --- a/src/components/close-button/close-button.jsx +++ b/src/components/close-button/close-button.jsx @@ -7,6 +7,8 @@ import closeIcon from './icon--close.svg'; import closeIconOrange from './icon--close-orange.svg'; import backIcon from '../../lib/assets/icon--back.svg'; +let closeIcons = {}; + const CloseButton = props => (
( className={classNames( styles.closeIcon, { - [styles.orange]: props.color === CloseButton.COLOR_ORANGE + [styles[props.color]]: (props.color !== CloseButton.COLOR_NEUTRAL) } )} - src={props.color === CloseButton.COLOR_ORANGE ? - closeIconOrange : + src={(props.color && closeIcons[props.color]) ? + closeIcons[props.color] : closeIcon } /> @@ -46,7 +48,16 @@ const CloseButton = props => ( CloseButton.SIZE_SMALL = 'small'; CloseButton.SIZE_LARGE = 'large'; + +CloseButton.COLOR_NEUTRAL = 'neutral'; +CloseButton.COLOR_GREEN = 'green'; CloseButton.COLOR_ORANGE = 'orange'; +closeIcons = { + [CloseButton.COLOR_NEUTRAL]: closeIcon, + [CloseButton.COLOR_GREEN]: closeIcon, // TODO: temporary, need green icon + [CloseButton.COLOR_ORANGE]: closeIconOrange +}; + CloseButton.propTypes = { buttonType: PropTypes.oneOf(['back', 'close']), @@ -57,6 +68,7 @@ CloseButton.propTypes = { }; CloseButton.defaultProps = { + color: CloseButton.COLOR_NEUTRAL, size: CloseButton.SIZE_LARGE, buttonType: 'close' }; diff --git a/src/components/coming-soon/coming-soon.css b/src/components/coming-soon/coming-soon.css index 900173e42..0390d6403 100644 --- a/src/components/coming-soon/coming-soon.css +++ b/src/components/coming-soon/coming-soon.css @@ -5,12 +5,13 @@ */ @import "../../css/colors.css"; +@import "../../css/units.css"; @import "../../css/z-index.css"; .coming-soon { background-color: $data-primary !important; border: 1px solid $ui-black-transparent !important; - border-radius: .25rem !important; + border-radius: $form-radius !important; box-shadow: 0 0 .5rem $ui-black-transparent !important; padding: .75rem 1rem !important; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; @@ -25,7 +26,7 @@ border-left: 0 !important; border-bottom: 0 !important; border-right: 1px solid $ui-black-transparent !important; - border-radius: .25rem; + border-radius: $form-radius; background-color: $data-primary !important; height: 1rem !important; width: 1rem !important; diff --git a/src/components/connection-modal/auto-scanning-step.jsx b/src/components/connection-modal/auto-scanning-step.jsx index a73c0b58d..b6f0dd813 100644 --- a/src/components/connection-modal/auto-scanning-step.jsx +++ b/src/components/connection-modal/auto-scanning-step.jsx @@ -63,7 +63,7 @@ const AutoScanningStep = props => (
- + {props.phase === PHASES.prescan && ( ( )} - {props.phase === PHASES.prescan && ( - - )} - {props.phase === PHASES.pressbutton && ( -
+ + {props.phase === PHASES.prescan && ( + )} + {props.phase === PHASES.pressbutton && ( +
+ + +
+ )} + {props.phase === PHASES.notfound && ( -
- )} - {props.phase === PHASES.notfound && ( - - )} + )} +
); diff --git a/src/components/connection-modal/connected-step.jsx b/src/components/connection-modal/connected-step.jsx index aa71c63c8..f71b3556e 100644 --- a/src/components/connection-modal/connected-step.jsx +++ b/src/components/connection-modal/connected-step.jsx @@ -25,7 +25,7 @@ const ConnectedStep = props => ( - + ( -
+
); @@ -207,7 +202,7 @@ LibraryComponent.propTypes = { name: PropTypes.oneOfType([ PropTypes.string, PropTypes.node - ]).isRequired, + ]), rawURL: PropTypes.string }) /* eslint-enable react/no-unused-prop-types, lines-around-comment */ diff --git a/src/components/loader/loader.css b/src/components/loader/loader.css index 1b6ab40ce..b57aa4c3a 100644 --- a/src/components/loader/loader.css +++ b/src/components/loader/loader.css @@ -2,7 +2,7 @@ @import "../../css/z-index.css"; .background { - position: fixed; + position: absolute; top: 0; left: 0; width: 100%; @@ -17,6 +17,13 @@ color: white; } +.fullscreen { + /* Break out of the layout using position: fixed to cover the whole screen */ + position: fixed; + /* Use the fullscreen stage z-index to allow covering full-screen mode */ + z-index: $z-index-stage-wrapper-overlay; +} + .block-animation { width: 125px; height: 150px; diff --git a/src/components/loader/loader.jsx b/src/components/loader/loader.jsx index dea1ff311..6f4b9658a 100644 --- a/src/components/loader/loader.jsx +++ b/src/components/loader/loader.jsx @@ -1,6 +1,8 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; +import classNames from 'classnames'; import styles from './loader.css'; +import PropTypes from 'prop-types'; import topBlock from './top-block.svg'; import middleBlock from './middle-block.svg'; @@ -97,6 +99,22 @@ const messages = [ weight: 1 } ]; +const mainMessages = { + 'gui.loader.headline': ( + + ), + 'gui.loader.creating': ( + + ) +}; class LoaderComponent extends React.Component { constructor (props) { @@ -131,7 +149,11 @@ class LoaderComponent extends React.Component { } render () { return ( -
+
- + {mainMessages[this.props.messageId]}
{thumbnailUrl ? ( - ) : null} diff --git a/src/components/menu-bar/author-info.css b/src/components/menu-bar/author-info.css new file mode 100644 index 000000000..dcec5986c --- /dev/null +++ b/src/components/menu-bar/author-info.css @@ -0,0 +1,39 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; + +.author-info { + color: $ui-white; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + display: flex; + justify-content: start; + align-items: center; + cursor: default; +} + +.avatar { + margin-right: .5625rem; +} + +.project-title { + max-width: $menu-bar-item-max-width; + display: block; + overflow: hidden; + font-size: $menu-bar-large-font-size; + font-weight: bold; + text-overflow: ellipsis; + white-space: nowrap; +} + +.username-line { + max-width: $menu-bar-item-max-width; + font-size: $menu-bar-standard-font-size; + display: block; + overflow: hidden; + font-weight: normal; + text-overflow: ellipsis; + white-space: nowrap; +} + +.username { + font-weight: bold; +} diff --git a/src/components/menu-bar/author-info.jsx b/src/components/menu-bar/author-info.jsx new file mode 100644 index 000000000..1b8c9a747 --- /dev/null +++ b/src/components/menu-bar/author-info.jsx @@ -0,0 +1,55 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import {FormattedMessage, injectIntl} from 'react-intl'; +import UserAvatar from './user-avatar.jsx'; + +import styles from './author-info.css'; + +const AuthorInfo = ({ + className, + imageUrl, + projectTitle, + // TODO: use userId to link to user's profile + userId, // eslint-disable-line no-unused-vars + username +}) => ( +
+ +
+ + {projectTitle} + +
+ + {username} + }} + /> + +
+
+
+); + +AuthorInfo.propTypes = { + className: PropTypes.string, + imageUrl: PropTypes.string, + projectTitle: PropTypes.string, + userId: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + username: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]) +}; + +export default injectIntl(AuthorInfo); diff --git a/src/components/menu-bar/community-button.css b/src/components/menu-bar/community-button.css new file mode 100644 index 000000000..9facb4bc4 --- /dev/null +++ b/src/components/menu-bar/community-button.css @@ -0,0 +1,9 @@ +@import "../../css/colors.css"; + +.community-button { + box-shadow: 0 0 0 1px $ui-black-transparent; +} + +.community-button-icon { + height: 1.25rem; +} diff --git a/src/components/menu-bar/community-button.jsx b/src/components/menu-bar/community-button.jsx new file mode 100644 index 000000000..62bf22e40 --- /dev/null +++ b/src/components/menu-bar/community-button.jsx @@ -0,0 +1,40 @@ +import classNames from 'classnames'; +import {FormattedMessage} from 'react-intl'; +import PropTypes from 'prop-types'; +import React from 'react'; +import Button from '../button/button.jsx'; + +import communityIcon from './icon--see-community.svg'; +import styles from './community-button.css'; + +const CommunityButton = ({ + className, + onClick +}) => ( + +); + +CommunityButton.propTypes = { + className: PropTypes.string, + onClick: PropTypes.func +}; + +CommunityButton.defaultProps = { + onClick: () => {} +}; + +export default CommunityButton; diff --git a/src/components/menu-bar/login-dropdown.jsx b/src/components/menu-bar/login-dropdown.jsx index 0116d7342..fcfcd8117 100644 --- a/src/components/menu-bar/login-dropdown.jsx +++ b/src/components/menu-bar/login-dropdown.jsx @@ -35,6 +35,11 @@ const LoginDropdownMessages = defineMessages({ // eslint-disable-line no-unused- defaultMessage: 'Need Help?', description: 'Button text for user to indicate that they need help', id: 'login.needHelp' + }, + validationRequired: { + defaultMessage: 'This field is required', + description: 'Message to tell user they must enter text in a form field', + id: 'form.validationRequired' } }); diff --git a/src/components/menu-bar/menu-bar.css b/src/components/menu-bar/menu-bar.css index 29902eabb..aa893a097 100644 --- a/src/components/menu-bar/menu-bar.css +++ b/src/components/menu-bar/menu-bar.css @@ -25,7 +25,7 @@ width: 100%; */ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: .75rem; + font-size: $menu-bar-standard-font-size; font-weight: bold; background-color: $motion-primary; color: $ui-white; @@ -45,6 +45,10 @@ vertical-align: middle; } +.scratch-logo.clickable { + cursor: pointer; +} + .language-icon { height: 1.5rem; vertical-align: middle; @@ -83,7 +87,7 @@ } .menu-bar-item.growable { - max-width: 12rem; + max-width: $menu-bar-item-max-width; flex: 1; } @@ -126,24 +130,13 @@ height: 34px; } +.author-info { + margin-left: .25rem; + margin-right: .6875rem; +} + .menu-bar-button { - height: 2rem; -} - -.share-button { - background: $data-primary; -} - -.share-button-is-shared { - background: $ui-black-transparent; -} - -.community-button { - box-shadow: 0 0 0 1px $ui-black-transparent; -} - -.community-button-icon { - height: 1.25rem; + height: $menu-bar-button-size; } .remix-button { @@ -186,8 +179,8 @@ .profile-icon { margin: 0 .25rem; - width: 2rem; - border-radius: 0.25rem; + width: $menu-bar-button-size; + border-radius: $form-radius; } .dropdown-caret-icon { @@ -207,19 +200,6 @@ opacity: 0.5; } -.save-in-progress { - animation: hue-rotate 3s linear infinite; -} - -@keyframes hue-rotate { - from { - filter: hue-rotate(); - } - to { - filter: hue-rotate(360deg); - } -} - .mystuff > a { background-repeat: no-repeat; background-position: center center; diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index 87af1e683..825f6096f 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -7,13 +7,18 @@ import React from 'react'; import Box from '../box/box.jsx'; import Button from '../button/button.jsx'; +import CommunityButton from './community-button.jsx'; +import ShareButton from './share-button.jsx'; import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx'; import Divider from '../divider/divider.jsx'; import LanguageSelector from '../../containers/language-selector.jsx'; +import SaveStatus from './save-status.jsx'; import SBFileUploader from '../../containers/sb-file-uploader.jsx'; +import ProjectWatcher from '../../containers/project-watcher.jsx'; import MenuBarMenu from './menu-bar-menu.jsx'; import {MenuItem, MenuSection} from '../menu/menu.jsx'; import ProjectTitleInput from './project-title-input.jsx'; +import AuthorInfo from './author-info.jsx'; import AccountNav from '../../containers/account-nav.jsx'; import LoginDropdown from './login-dropdown.jsx'; import SB3Downloader from '../../containers/sb3-downloader.jsx'; @@ -23,11 +28,12 @@ import TurboMode from '../../containers/turbo-mode.jsx'; import {openTipsLibrary} from '../../reducers/modals'; import {setPlayer} from '../../reducers/mode'; import { + autoUpdateProject, getIsUpdating, getIsShowingProject, + manualUpdateProject, requestNewProject, remixProject, - updateProject, saveProjectAsCopy } from '../../reducers/project-state'; import { @@ -54,13 +60,19 @@ import helpIcon from '../../lib/assets/icon--tutorials.svg'; import mystuffIcon from './icon--mystuff.png'; import feedbackIcon from './icon--feedback.svg'; import profileIcon from './icon--profile.png'; -import communityIcon from './icon--see-community.svg'; import remixIcon from './icon--remix.svg'; import dropdownCaret from './dropdown-caret.svg'; import languageIcon from '../language-selector/language-icon.svg'; import scratchLogo from './scratch-logo.svg'; +const messages = defineMessages({ + confirmNav: { + id: 'gui.menuBar.confirmNewWithoutSaving', + defaultMessage: 'Replace contents of the current project?', + description: 'message for prompting user to confirm that they want to create new project without saving' + } +}); const ariaMessages = defineMessages({ language: { id: 'gui.menuBar.LanguageSelector', @@ -136,37 +148,64 @@ class MenuBar extends React.Component { 'handleClickRemix', 'handleClickSave', 'handleClickSaveAsCopy', + 'handleClickSeeCommunity', + 'handleClickShare', 'handleCloseFileMenuAndThen', 'handleLanguageMouseUp', 'handleRestoreOption', 'restoreOptionMessage' ]); } - componentDidUpdate (prevProps) { - // if we're no longer showing the project (loading, or whatever), close menus - if (this.props.isShowingProject && !prevProps.isShowingProject) { - this.props.onRequestCloseFile(); - this.props.onRequestCloseEdit(); - } - } handleClickNew () { - // if canSave===true and canCreateNew===true, it's safe to replace current project, - // since we will auto-save first. Else, confirm first. - const readyToReplaceProject = (this.props.canSave && this.props.canCreateNew) || - confirm('Replace contents of the current project?'); // eslint-disable-line no-alert + let readyToReplaceProject = true; + // if the project is dirty, and user owns the project, we will autosave. + // but if they are not logged in and can't save, user should consider + // downloading or logging in first. + // Note that if user is logged in and editing someone else's project, + // they'll lose their work. + if (this.props.projectChanged && !this.props.canCreateNew) { + readyToReplaceProject = confirm( // eslint-disable-line no-alert + this.props.intl.formatMessage(messages.confirmNav) + ); + } this.props.onRequestCloseFile(); if (readyToReplaceProject) { this.props.onClickNew(this.props.canSave && this.props.canCreateNew); } + this.props.onRequestCloseFile(); } handleClickRemix () { this.props.onClickRemix(); + this.props.onRequestCloseFile(); } handleClickSave () { this.props.onClickSave(); + this.props.onRequestCloseFile(); } handleClickSaveAsCopy () { this.props.onClickSaveAsCopy(); + this.props.onRequestCloseFile(); + } + handleClickSeeCommunity (waitForUpdate) { + if (this.props.canSave) { // save before transitioning to project page + this.props.autoUpdateProject(); + waitForUpdate(true); // queue the transition to project page + } else { + waitForUpdate(false); // immediately transition to project page + } + } + handleClickShare (waitForUpdate) { + if (!this.props.isShared) { + if (this.props.canShare) { // save before transitioning to project page + this.props.onShare(); + } + if (this.props.canSave) { // save before transitioning to project page + this.props.autoUpdateProject(); + waitForUpdate(true); // queue the transition to project page + } else { + waitForUpdate(false); // immediately transition to project page + } + } } handleRestoreOption (restoreFun) { return () => { @@ -243,32 +282,6 @@ class MenuBar extends React.Component { id="gui.menuBar.new" /> ); - const shareMessage = ( - - ); - const isSharedMessage = ( - - ); - const shareButton = ( - - ); const remixButton = ( + (this.props.isShowingProject || this.props.isUpdating) && ( + + { + waitForUpdate => ( + { + this.handleClickSeeCommunity(waitForUpdate); + }} + /* eslint-enable react/jsx-no-bind */ + /> + ) + } + + ) ) : (this.props.showComingSoon ? ( - + ) : [])}
@@ -520,6 +547,11 @@ class MenuBar extends React.Component { {/* show the proper UI in the account menu, given whether the user is logged in, and whether a session is available to log in with */}
+
+ {this.props.canSave && ( + + )} +
{this.props.sessionExists ? ( this.props.username ? ( // ************ user is logged in ************ @@ -667,8 +699,13 @@ class MenuBar extends React.Component { MenuBar.propTypes = { accountMenuOpen: PropTypes.bool, + authorId: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + authorThumbnailUrl: PropTypes.string, + authorUsername: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + autoUpdateProject: PropTypes.func, canCreateCopy: PropTypes.bool, canCreateNew: PropTypes.bool, + canEditTitle: PropTypes.bool, canRemix: PropTypes.bool, canSave: PropTypes.bool, canShare: PropTypes.bool, @@ -688,6 +725,7 @@ MenuBar.propTypes = { onClickFile: PropTypes.func, onClickLanguage: PropTypes.func, onClickLogin: PropTypes.func, + onClickLogo: PropTypes.func, onClickNew: PropTypes.func, onClickRemix: PropTypes.func, onClickSave: PropTypes.func, @@ -704,6 +742,8 @@ MenuBar.propTypes = { onShare: PropTypes.func, onToggleLoginOpen: PropTypes.func, onUpdateProjectTitle: PropTypes.func, + projectChanged: PropTypes.bool, + projectTitle: PropTypes.string, renderLogin: PropTypes.func, sessionExists: PropTypes.bool, showComingSoon: PropTypes.bool, @@ -726,12 +766,15 @@ const mapStateToProps = state => { isShowingProject: getIsShowingProject(loadingState), languageMenuOpen: languageMenuOpen(state), loginMenuOpen: loginMenuOpen(state), + projectChanged: state.scratchGui.projectChanged, + projectTitle: state.scratchGui.projectTitle, sessionExists: state.session && typeof state.session.session !== 'undefined', username: user ? user.username : null }; }; const mapDispatchToProps = dispatch => ({ + autoUpdateProject: () => dispatch(autoUpdateProject()), onOpenTipLibrary: () => dispatch(openTipsLibrary()), onClickAccount: () => dispatch(openAccountMenu()), onRequestCloseAccount: () => dispatch(closeAccountMenu()), @@ -745,7 +788,7 @@ const mapDispatchToProps = dispatch => ({ onRequestCloseLogin: () => dispatch(closeLoginMenu()), onClickNew: needSave => dispatch(requestNewProject(needSave)), onClickRemix: () => dispatch(remixProject()), - onClickSave: () => dispatch(updateProject()), + onClickSave: () => dispatch(manualUpdateProject()), onClickSaveAsCopy: () => dispatch(saveProjectAsCopy()), onSeeCommunity: () => dispatch(setPlayer(true)) }); diff --git a/src/components/menu-bar/save-status.css b/src/components/menu-bar/save-status.css new file mode 100644 index 000000000..d191d5d23 --- /dev/null +++ b/src/components/menu-bar/save-status.css @@ -0,0 +1,4 @@ +.save-now { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + cursor: pointer; +} diff --git a/src/components/menu-bar/save-status.jsx b/src/components/menu-bar/save-status.jsx new file mode 100644 index 000000000..9f83f6274 --- /dev/null +++ b/src/components/menu-bar/save-status.jsx @@ -0,0 +1,61 @@ +import {connect} from 'react-redux'; +import {FormattedMessage} from 'react-intl'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import InlineMessages from '../../containers/inline-messages.jsx'; + +import { + manualUpdateProject +} from '../../reducers/project-state'; + +import { + filterInlineAlerts +} from '../../reducers/alerts'; + +import styles from './save-status.css'; + +// Wrapper for inline messages in the nav bar, which are all related to saving. +// Show any inline messages if present, else show the "Save Now" button if the +// project has changed. +// We decided to not use an inline message for "Save Now" because it is a reflection +// of the project state, rather than an event. +const SaveStatus = ({ + alertsList, + projectChanged, + onClickSave +}) => ( + filterInlineAlerts(alertsList).length > 0 ? ( + + ) : projectChanged && ( +
+ +
+ )); + +SaveStatus.propTypes = { + alertsList: PropTypes.arrayOf(PropTypes.object), + onClickSave: PropTypes.func, + projectChanged: PropTypes.bool +}; + +const mapStateToProps = state => ({ + alertsList: state.scratchGui.alerts.alertsList, + projectChanged: state.scratchGui.projectChanged +}); + +const mapDispatchToProps = dispatch => ({ + onClickSave: () => dispatch(manualUpdateProject()) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(SaveStatus); diff --git a/src/components/menu-bar/share-button.css b/src/components/menu-bar/share-button.css new file mode 100644 index 000000000..09452684c --- /dev/null +++ b/src/components/menu-bar/share-button.css @@ -0,0 +1,10 @@ +@import "../../css/colors.css"; + +.share-button { + background: $data-primary; +} + +.share-button-is-shared { + background: $ui-black-transparent; + cursor: default; +} diff --git a/src/components/menu-bar/share-button.jsx b/src/components/menu-bar/share-button.jsx new file mode 100644 index 000000000..7513867ee --- /dev/null +++ b/src/components/menu-bar/share-button.jsx @@ -0,0 +1,48 @@ +import classNames from 'classnames'; +import {FormattedMessage} from 'react-intl'; +import PropTypes from 'prop-types'; +import React from 'react'; +import Button from '../button/button.jsx'; + +import styles from './share-button.css'; + +const ShareButton = ({ + className, + isShared, + onClick +}) => ( + +); + +ShareButton.propTypes = { + className: PropTypes.string, + isShared: PropTypes.bool, + onClick: PropTypes.func +}; + +ShareButton.defaultProps = { + onClick: () => {} +}; + +export default ShareButton; diff --git a/src/components/menu-bar/user-avatar.css b/src/components/menu-bar/user-avatar.css new file mode 100644 index 000000000..2d1ac6fef --- /dev/null +++ b/src/components/menu-bar/user-avatar.css @@ -0,0 +1,10 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; + +.user-thumbnail { + width: $menu-bar-button-size; + height: $menu-bar-button-size; + border-radius: $form-radius; + vertical-align: middle; + box-shadow: 0 0 0 1px $ui-black-transparent; +} diff --git a/src/components/menu-bar/user-avatar.jsx b/src/components/menu-bar/user-avatar.jsx new file mode 100644 index 000000000..435130ec9 --- /dev/null +++ b/src/components/menu-bar/user-avatar.jsx @@ -0,0 +1,25 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import styles from './user-avatar.css'; + +const UserAvatar = ({ + className, + imageUrl +}) => ( + +); + +UserAvatar.propTypes = { + className: PropTypes.string, + imageUrl: PropTypes.string +}; + +export default UserAvatar; diff --git a/src/components/meter/meter.css b/src/components/meter/meter.css index c16e920cd..953f3641b 100644 --- a/src/components/meter/meter.css +++ b/src/components/meter/meter.css @@ -1,3 +1,5 @@ +@import "../../css/colors.css"; + .green { fill: rgb(171, 220, 170); stroke: rgb(174, 211, 168); @@ -12,3 +14,19 @@ fill: rgb(251, 194, 142); stroke: rgb(235, 189, 142); } + +.mask-container { + position: relative; +} + +.mask { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + transform-origin: top; + will-change: transform; + background: $ui-primary; + opacity: 0.75; +} diff --git a/src/components/meter/meter.jsx b/src/components/meter/meter.jsx index 2d16ce681..4f0711916 100644 --- a/src/components/meter/meter.jsx +++ b/src/components/meter/meter.jsx @@ -20,36 +20,40 @@ const Meter = props => { const barHeight = (height - (barSpacing * (nBars + 1))) / nBars; const nBarsToMask = nBars - Math.floor(level * nBars); + const scale = ((nBarsToMask * (barHeight + barSpacing)) + (barSpacing / 2)) / height; return ( - - {Array(nBars).fill(0) - .map((value, index) => ( - - ))} - + {Array(nBars).fill(0) + .map((value, index) => ( + + ))} + +
- +
); }; diff --git a/src/components/monitor/monitor.jsx b/src/components/monitor/monitor.jsx index 7be0875e5..221343275 100644 --- a/src/components/monitor/monitor.jsx +++ b/src/components/monitor/monitor.jsx @@ -19,7 +19,8 @@ const categories = { sound: '#CF63CF', looks: '#9966FF', motion: '#4C97FF', - list: '#FC662C' + list: '#FC662C', + extension: '#0FBD8C' }; const modes = { @@ -53,35 +54,52 @@ const MonitorComponent = props => ( })} - {props.mode === 'list' ? null : ReactDOM.createPortal(( + {ReactDOM.createPortal(( // Use a portal to render the context menu outside the flow to avoid // positioning conflicts between the monitors `transform: scale` and // the context menus `position: fixed`. For more details, see // http://meyerweb.com/eric/thoughts/2011/09/12/un-fixing-fixed-elements-with-css-transforms/ - - - - - - - {props.onSetModeToSlider ? ( + {props.onSetModeToDefault && + + + } + {props.onSetModeToLarge && + + + } + {props.onSetModeToSlider && - - ) : null} + } + {props.onImport && + + + } + {props.onExport && + + + } ), document.body)} @@ -99,14 +117,16 @@ MonitorComponent.propTypes = { label: PropTypes.string.isRequired, mode: PropTypes.oneOf(monitorModes), onDragEnd: PropTypes.func.isRequired, + onExport: PropTypes.func, + onImport: PropTypes.func, onNextMode: PropTypes.func.isRequired, - onSetModeToDefault: PropTypes.func.isRequired, - onSetModeToLarge: PropTypes.func.isRequired, + onSetModeToDefault: PropTypes.func, + onSetModeToLarge: PropTypes.func, onSetModeToSlider: PropTypes.func }; MonitorComponent.defaultProps = { - category: 'data', + category: 'extension', mode: 'default' }; diff --git a/src/components/prompt/prompt.css b/src/components/prompt/prompt.css index 911271f19..c12afc6b7 100644 --- a/src/components/prompt/prompt.css +++ b/src/components/prompt/prompt.css @@ -10,11 +10,20 @@ padding: 1.5rem 2.25rem; } +.body input[type="checkbox"], +.body input[type="radio"] { + margin: 3px; +} + .label { font-weight: 500; margin: 0 0 0.75rem; } +.disabled-label { + opacity: 0.5; +} + .variable-name-text-input { margin-bottom: 1.5rem; width: 100%; @@ -68,35 +77,16 @@ margin-right: 0.5rem; } -.more-options { +.cloud-option { + display:flex; border-top: 1px dashed hsla(0, 0%, 0%, .25); overflow: visible; - padding: 1rem; + padding: 1rem 0; text-align: center; - margin: 0 0 1rem; -} - -.more-options-accordion { - width: 60%; + width: 100%; margin: 0 auto; } -.more-options-text { +.cloud-option-text { opacity: .5; } - -.more-options-icon { - width: .75rem; - height: .75rem; - vertical-align: middle; - padding-bottom: .2rem; - opacity: .5; -} - -[dir="ltr"] .more-options-icon { - margin-left: .5rem; -} - -[dir="rtl"] .more-options-icon { - margin-right: .5rem; -} diff --git a/src/components/prompt/prompt.jsx b/src/components/prompt/prompt.jsx index 78192f3ad..d60af1f34 100644 --- a/src/components/prompt/prompt.jsx +++ b/src/components/prompt/prompt.jsx @@ -1,14 +1,13 @@ +import classNames from 'classnames'; import {defineMessages, FormattedMessage} from 'react-intl'; import PropTypes from 'prop-types'; import React from 'react'; import Box from '../box/box.jsx'; -import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx'; import Modal from '../../containers/modal.jsx'; import styles from './prompt.css'; -import dropdownIcon from './icon--dropdown-caret.svg'; const messages = defineMessages({ forAllSpritesMessage: { @@ -21,10 +20,10 @@ const messages = defineMessages({ description: 'Option message when creating a varaible for making it only available to the current sprite', id: 'gui.gui.variableScopeOptionSpriteOnly' }, - moreOptionsMessage: { - defaultMessage: 'More Options', - description: 'Dropdown message for variable/list options', - id: 'gui.gui.variablePrompt' + cloudVarOptionMessage: { + defaultMessage: 'Cloud variable (stored on server)', + description: 'Option message when creating a variable for making it a cloud variable, a variable that is stored on the server', /* eslint-disable-line max-len */ + id: 'gui.gui.cloudVariableOption' }, availableToAllSpritesMessage: { defaultMessage: 'This variable will be available to all sprites.', @@ -48,13 +47,14 @@ const PromptComponent = props => ( - {props.showMoreOptions ? + {props.showVariableOptions ?
{props.isStage ?
@@ -65,45 +65,48 @@ const PromptComponent = props => ( - } - - -
+ {props.showCloudOption ? + +
-
-
+ + : null}
: null} @@ -133,15 +136,21 @@ const PromptComponent = props => ( ); PromptComponent.propTypes = { + canAddCloudVariable: PropTypes.bool.isRequired, + cloudSelected: PropTypes.bool.isRequired, + defaultValue: PropTypes.string, + globalSelected: PropTypes.bool.isRequired, isStage: PropTypes.bool.isRequired, label: PropTypes.string.isRequired, onCancel: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, + onCloudVarOptionChange: PropTypes.func, + onFocus: PropTypes.func.isRequired, onKeyPress: PropTypes.func.isRequired, onOk: PropTypes.func.isRequired, - onOptionSelection: PropTypes.func.isRequired, - placeholder: PropTypes.string, - showMoreOptions: PropTypes.bool.isRequired, + onScopeOptionSelection: PropTypes.func.isRequired, + showCloudOption: PropTypes.bool.isRequired, + showVariableOptions: PropTypes.bool.isRequired, title: PropTypes.string.isRequired }; diff --git a/src/components/sound-editor/sound-editor.css b/src/components/sound-editor/sound-editor.css index 4a0e03655..279875fc8 100644 --- a/src/components/sound-editor/sound-editor.css +++ b/src/components/sound-editor/sound-editor.css @@ -79,6 +79,7 @@ $border-radius: 0.25rem; font-size: 0.85rem; transition: 0.2s; user-select: none; + margin: 0px; } .button > img { diff --git a/src/components/spinner/spinner.css b/src/components/spinner/spinner.css index 89d225348..3abc8ffbb 100644 --- a/src/components/spinner/spinner.css +++ b/src/components/spinner/spinner.css @@ -1,19 +1,20 @@ @import "../../css/colors.css"; .spinner { - width: 1rem; - height: 1rem; + width: 1.25rem; + height: 1.25rem; display: inline-block; position: relative; border-radius: 50%; border-width: .1875rem; border-style: solid; border-color: $ui-white-transparent; + box-sizing: content-box; } -.spinner::after, .spinner::before { - width: .625rem; - height: .625rem; +.spinner::before, .spinner::after { + width: 1.25rem; + height: 1.25rem; content: ''; border-radius: 50%; display: block; @@ -28,6 +29,16 @@ animation: spin 1.5s cubic-bezier(0.4, 0.1, 0.4, 1) infinite; } +.small { + width: .5rem; + height: .5rem; +} + +.small::before, .small::after { + width: .5rem; + height: .5rem; +} + @keyframes spin { 0% { transform: rotate(0deg); @@ -38,17 +49,25 @@ } } -.spinner.orange { +.spinner.success { + border-color: $extensions-transparent; +} + +.spinner.success::after { + border-top-color: $extensions-primary; +} + +.spinner.warn { border-color: $error-transparent; } -.spinner.orange::after { +.spinner.warn::after { border-top-color: $error-primary; } -.spinner.white { +.spinner.info { border-color: $ui-white-transparent; } -.spinner.white::after { +.spinner.info::after { border-top-color: $ui-white; } diff --git a/src/components/spinner/spinner.jsx b/src/components/spinner/spinner.jsx index 08685ccb3..f25e3ebb1 100644 --- a/src/components/spinner/spinner.jsx +++ b/src/components/spinner/spinner.jsx @@ -6,21 +6,28 @@ import styles from './spinner.css'; const SpinnerComponent = function (props) { const { - className + className, + level, + small } = props; return (
); }; SpinnerComponent.propTypes = { - className: PropTypes.string + className: PropTypes.string, + level: PropTypes.string, + small: PropTypes.bool }; SpinnerComponent.defaultProps = { - className: '' + className: '', + level: 'info' }; export default SpinnerComponent; diff --git a/src/components/sprite-info/sprite-info.jsx b/src/components/sprite-info/sprite-info.jsx index e9090bfe3..09c94d016 100644 --- a/src/components/sprite-info/sprite-info.jsx +++ b/src/components/sprite-info/sprite-info.jsx @@ -34,14 +34,15 @@ 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 || - this.props.size !== nextProps.size || this.props.stageSize !== nextProps.stageSize || this.props.visible !== nextProps.visible || - this.props.x !== nextProps.x || - this.props.y !== nextProps.y + // Only update these if rounded value has changed + Math.round(this.props.direction) !== Math.round(nextProps.direction) || + Math.round(this.props.size) !== Math.round(nextProps.size) || + Math.round(this.props.x) !== Math.round(nextProps.x) || + Math.round(this.props.y) !== Math.round(nextProps.y) ); } render () { @@ -110,7 +111,7 @@ class SpriteInfo extends React.Component { placeholder="x" tabIndex="0" type="text" - value={this.props.disabled ? '' : this.props.x} + value={this.props.disabled ? '' : Math.round(this.props.x)} onSubmit={this.props.onChangeX} /> @@ -137,7 +138,7 @@ class SpriteInfo extends React.Component { placeholder="y" tabIndex="0" type="text" - value={this.props.disabled ? '' : this.props.y} + value={this.props.disabled ? '' : Math.round(this.props.y)} onSubmit={this.props.onChangeY} /> @@ -237,14 +238,14 @@ class SpriteInfo extends React.Component { label={sizeLabel} tabIndex="0" type="text" - value={this.props.disabled ? '' : this.props.size} + value={this.props.disabled ? '' : Math.round(this.props.size)} onSubmit={this.props.onChangeSize} />
( disable={props.dragging} id={`${props.name}-${contextMenuId}`} > - {(props.selected && props.onDeleteButtonClick) ? ( - - ) : null } {typeof props.number === 'undefined' ? null : (
{props.number}
)} {props.costumeURL ? ( - +
+
+ +
+
) : null}
{props.name}
@@ -49,6 +46,13 @@ const SpriteSelectorItem = props => (
{props.details}
) : null}
+ {(props.selected && props.onDeleteButtonClick) ? ( + + ) : null } {props.onDuplicateButtonClick || props.onDeleteButtonClick || props.onExportButtonClick ? ( {props.onDuplicateButtonClick ? ( @@ -60,15 +64,6 @@ const SpriteSelectorItem = props => ( /> ) : null} - {props.onDeleteButtonClick ? ( - - - - ) : null } {props.onExportButtonClick ? ( ( /> ) : null } + {props.onDeleteButtonClick ? ( + + + + ) : null } ) : null} diff --git a/src/components/sprite-selector/sprite-list.jsx b/src/components/sprite-selector/sprite-list.jsx index 17ba2dc21..e89898e47 100644 --- a/src/components/sprite-selector/sprite-list.jsx +++ b/src/components/sprite-selector/sprite-list.jsx @@ -8,9 +8,12 @@ import Box from '../box/box.jsx'; import SpriteSelectorItem from '../../containers/sprite-selector-item.jsx'; import SortableHOC from '../../lib/sortable-hoc.jsx'; import SortableAsset from '../asset-panel/sortable-asset.jsx'; +import ThrottledPropertyHOC from '../../lib/throttled-property-hoc.jsx'; import styles from './sprite-selector.css'; +const ThrottledSpriteSelectorItem = ThrottledPropertyHOC('asset', 500)(SpriteSelectorItem); + const SpriteList = function (props) { const { containerRef, @@ -34,62 +37,68 @@ const SpriteList = function (props) { return ( - {items.map((sprite, index) => { + + {items.map((sprite, index) => { - // If the sprite has just received a block drop, used for green highlight - const receivedBlocks = ( - hoveredTarget.sprite === sprite.id && + // If the sprite has just received a block drop, used for green highlight + const receivedBlocks = ( + hoveredTarget.sprite === sprite.id && sprite.id !== editingTarget && hoveredTarget.receivedBlocks - ); + ); - // If the sprite is indicating it can receive block dropping, used for blue highlight - let isRaised = !receivedBlocks && raised && sprite.id !== editingTarget; + // If the sprite is indicating it can receive block dropping, used for blue highlight + let isRaised = !receivedBlocks && raised && sprite.id !== editingTarget; - // A sprite is also raised if a costume or sound is being dragged. - // Note the absence of the self-sharing check: a sprite can share assets with itself. - // This is a quirk of 2.0, but seems worth leaving possible, it - // allows quick (albeit unusual) duplication of assets. - isRaised = isRaised || [ - DragConstants.COSTUME, - DragConstants.SOUND, - DragConstants.BACKPACK_COSTUME, - DragConstants.BACKPACK_SOUND, - DragConstants.BACKPACK_CODE].includes(draggingType); + // A sprite is also raised if a costume or sound is being dragged. + // Note the absence of the self-sharing check: a sprite can share assets with itself. + // This is a quirk of 2.0, but seems worth leaving possible, it + // allows quick (albeit unusual) duplication of assets. + isRaised = isRaised || [ + DragConstants.COSTUME, + DragConstants.SOUND, + DragConstants.BACKPACK_COSTUME, + DragConstants.BACKPACK_SOUND, + DragConstants.BACKPACK_CODE].includes(draggingType); - return ( - - - - ); - })} + return ( + + + + ); + })} + ); }; diff --git a/src/components/sprite-selector/sprite-selector.css b/src/components/sprite-selector/sprite-selector.css index da89efab0..ffade4750 100644 --- a/src/components/sprite-selector/sprite-selector.css +++ b/src/components/sprite-selector/sprite-selector.css @@ -31,6 +31,7 @@ */ box-sizing: border-box; width: calc((100% / $sprites-per-row ) - $space); + max-width: 6rem; min-width: 4rem; min-height: 4rem; /* @todo: calc height same as width */ margin: calc($space / 2); @@ -54,6 +55,10 @@ overflow-y: auto; } +.scroll-wrapper-dragging { + background-color: $drop-highlight; +} + .items-wrapper { display: flex; flex-wrap: wrap; @@ -79,12 +84,12 @@ } .raised { - background-color: #8cbcff; + background-color: $drop-highlight; transition: all 0.25s ease; } .raised:hover { - background-color: #8cbcff; + background-color: $drop-highlight; transform: scale(1.05); } @@ -93,7 +98,7 @@ animation-duration: 500ms; animation-iteration-count: 1; animation-timing-function: ease-in-out; - background-color: #8cbcff; + background-color: $drop-highlight; } @keyframes wiggle { diff --git a/src/components/sprite-selector/sprite-selector.jsx b/src/components/sprite-selector/sprite-selector.jsx index 981e44b0f..115eaee95 100644 --- a/src/components/sprite-selector/sprite-selector.jsx +++ b/src/components/sprite-selector/sprite-selector.jsx @@ -100,20 +100,18 @@ const SpriteSelectorComponent = function (props) { onChangeY={onChangeSpriteY} /> - - sprites[id])} - raised={raised} - selectedId={selectedId} - onDeleteSprite={onDeleteSprite} - onDrop={onDrop} - onDuplicateSprite={onDuplicateSprite} - onExportSprite={onExportSprite} - onSelectSprite={onSelectSprite} - /> - + sprites[id])} + raised={raised} + selectedId={selectedId} + onDeleteSprite={onDeleteSprite} + onDrop={onDrop} + onDuplicateSprite={onDuplicateSprite} + onExportSprite={onExportSprite} + onSelectSprite={onSelectSprite} + /> + + Scratch + +
+ ) : ( + + ); header = ( - + {stageButton} ); @@ -171,6 +189,7 @@ StageHeaderComponent.propTypes = { onSetStageLarge: PropTypes.func.isRequired, onSetStageSmall: PropTypes.func.isRequired, onSetStageUnFull: PropTypes.func.isRequired, + showBranding: PropTypes.bool.isRequired, stageSizeMode: PropTypes.oneOf(Object.keys(STAGE_SIZE_MODES)), vm: PropTypes.instanceOf(VM).isRequired }; diff --git a/src/components/stage-selector/stage-selector.css b/src/components/stage-selector/stage-selector.css index 8136911e8..742898bd3 100644 --- a/src/components/stage-selector/stage-selector.css +++ b/src/components/stage-selector/stage-selector.css @@ -100,7 +100,7 @@ $header-height: calc($stage-menu-height - 2px); } .raised, .raised .header { - background-color: #8cbcff; + background-color: $drop-highlight; transition: all 0.25s ease; } diff --git a/src/components/stage-wrapper/stage-wrapper.jsx b/src/components/stage-wrapper/stage-wrapper.jsx index 795b32247..77e9f5595 100644 --- a/src/components/stage-wrapper/stage-wrapper.jsx +++ b/src/components/stage-wrapper/stage-wrapper.jsx @@ -6,18 +6,25 @@ import Box from '../box/box.jsx'; import {STAGE_DISPLAY_SIZES} from '../../lib/layout-constants.js'; import StageHeader from '../../containers/stage-header.jsx'; import Stage from '../../containers/stage.jsx'; +import Loader from '../loader/loader.jsx'; import styles from './stage-wrapper.css'; const StageWrapperComponent = function (props) { const { + isFullScreen, + isRtl, isRendererSupported, + loading, stageSize, vm } = props; return ( - + + {loading ? ( + + ) : null} ); }; StageWrapperComponent.propTypes = { + isFullScreen: PropTypes.bool, isRendererSupported: PropTypes.bool.isRequired, + isRtl: PropTypes.bool.isRequired, + loading: PropTypes.bool, stageSize: PropTypes.oneOf(Object.keys(STAGE_DISPLAY_SIZES)).isRequired, vm: PropTypes.instanceOf(VM).isRequired }; diff --git a/src/components/stage/stage.css b/src/components/stage/stage.css index 9264c094e..37b322024 100644 --- a/src/components/stage/stage.css +++ b/src/components/stage/stage.css @@ -9,9 +9,9 @@ */ display: block; - /* Attach border radius directly to canvas to prevent needing overflow:hidden; */ border-radius: $space; border: $stage-standard-border-width solid $ui-black-transparent; + overflow: hidden; /* @todo: This is for overriding the value being set somewhere. Where is it being set? */ background-color: transparent; @@ -89,7 +89,10 @@ to adjust for the border using a different method */ padding-bottom: calc($stage-full-screen-stage-padding + $stage-full-screen-border-width); } -.monitor-wrapper, .color-picker-wrapper, .frame-wrapper { +.monitor-wrapper, +.color-picker-wrapper, +.frame-wrapper, +.green-flag-overlay-wrapper { position: absolute; top: 0; left: 0; @@ -138,6 +141,36 @@ to adjust for the border using a different method */ animation-fill-mode: forwards; /* Leave at 0 opacity after animation */ } +.green-flag-overlay-wrapper { + display: flex; + justify-content: center; + align-items: center; + background: rgba(0,0,0,0.25); + border-radius: 0.5rem; + pointer-events: all; + cursor: pointer; +} + +.green-flag-overlay { + padding: 1rem; + border-radius: 100%; + background: rgba(255,255,255,0.75); + border: 3px solid $ui-white; + display: flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + width: 5rem; + height: 5rem; +} + +.green-flag-overlay > img { + width: 100%; + object-fit: contain; +} + + + @keyframes flash { 0% { opacity: 1; } 100% { opacity: 0; } diff --git a/src/components/stage/stage.jsx b/src/components/stage/stage.jsx index ede5048ea..cf81f4ec2 100644 --- a/src/components/stage/stage.jsx +++ b/src/components/stage/stage.jsx @@ -7,6 +7,7 @@ import DOMElementRenderer from '../../containers/dom-element-renderer.jsx'; import Loupe from '../loupe/loupe.jsx'; import MonitorList from '../../containers/monitor-list.jsx'; import TargetHighlight from '../../containers/target-highlight.jsx'; +import GreenFlagOverlay from '../../containers/green-flag-overlay.jsx'; import Question from '../../containers/question.jsx'; import MicIndicator from '../mic-indicator/mic-indicator.jsx'; import {STAGE_DISPLAY_SIZES} from '../../lib/layout-constants.js'; @@ -19,6 +20,7 @@ const StageComponent = props => { dragRef, isColorPicking, isFullScreen, + isStarted, colorInfo, micIndicator, question, @@ -46,18 +48,25 @@ const StageComponent = props => { }} onDoubleClick={onDoubleClick} > - + > + + { stageWidth={stageDimensions.width} /> + {isStarted ? null : ( + + )} {isColorPicking && colorInfo ? ( @@ -125,6 +140,7 @@ StageComponent.propTypes = { dragRef: PropTypes.func, isColorPicking: PropTypes.bool, isFullScreen: PropTypes.bool.isRequired, + isStarted: PropTypes.bool, micIndicator: PropTypes.bool, onDeactivateColorPicker: PropTypes.func, onDoubleClick: PropTypes.func, diff --git a/src/components/stop-all/stop-all.css b/src/components/stop-all/stop-all.css index b2d878582..da0bcc7b3 100644 --- a/src/components/stop-all/stop-all.css +++ b/src/components/stop-all/stop-all.css @@ -1,3 +1,5 @@ +@import "../../css/colors.css"; + .stop-all { width: 2rem; height: 2rem; @@ -5,17 +7,16 @@ border-radius: 0.25rem; user-select: none; cursor: pointer; - transition: 0.2s ease-out; +} + +.stop-all:hover { + background-color: $motion-light-transparent; } .stop-all { opacity: 0.5; } -.stop-all:hover { - transform: scale(1.2); -} - .stop-all.is-active { opacity: 1; } diff --git a/src/components/telemetry-modal/telemetry-modal-header.png b/src/components/telemetry-modal/telemetry-modal-header.png new file mode 100644 index 000000000..01faec729 Binary files /dev/null and b/src/components/telemetry-modal/telemetry-modal-header.png differ diff --git a/src/components/telemetry-modal/telemetry-modal.css b/src/components/telemetry-modal/telemetry-modal.css new file mode 100644 index 000000000..9354e8fe0 --- /dev/null +++ b/src/components/telemetry-modal/telemetry-modal.css @@ -0,0 +1,82 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; +@import "../../css/typography.css"; +@import "../../css/z-index.css"; + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: $z-index-modal; + background-color: $ui-modal-overlay; +} + +.modal-content { + margin: 100px auto; + outline: none; + border: .25rem solid $ui-white-transparent; + padding: 0; + border-radius: $space; + user-select: none; + width: 640px; + + color: $text-primary; + overflow: hidden; +} + +.illustration { + width: 100%; + height: 123px; + background-color: $motion-primary; + background-image: url('./telemetry-modal-header.png'); + background-size: cover; +} + +.body { + background: $ui-white; + padding: 1.5rem 2.25rem; + text-align: left; +} + +.privacy-policy-link { + color: $motion-primary; + text-decoration: none; +} + +/* Confirmation buttons at the bottom of the modal */ +.button-row { + margin: 1.5rem 0; + font-weight: bolder; + text-align: right; + display: flex; + justify-content: center; +} + +.button-row button { + border: 1px solid $motion-primary; + border-radius: 0.25rem; + padding: 0.5rem 1.5rem; + background: white; + font-weight: bold; + font-size: .875rem; + cursor: pointer; +} + +.button-row button.opt-in { + background: $motion-primary; + color: white; +} + +.button-row button.opt-out { + color: $motion-primary; +} + +[dir="ltr"] .button-row button + button { + margin-left: 0.5rem; +} + +[dir="rtl"] .button-row button + button { + margin-right: 0.5rem; +} diff --git a/src/components/telemetry-modal/telemetry-modal.jsx b/src/components/telemetry-modal/telemetry-modal.jsx new file mode 100644 index 000000000..2d3149d3c --- /dev/null +++ b/src/components/telemetry-modal/telemetry-modal.jsx @@ -0,0 +1,140 @@ +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl'; +import ReactModal from 'react-modal'; + +import Box from '../box/box.jsx'; + +import styles from './telemetry-modal.css'; + +const messages = defineMessages({ + label: { + id: 'gui.telemetryOptIn.label', + defaultMessage: 'Report statistics to improve Scratch', + description: 'Scratch 3.0 telemetry modal label - for accessibility' + }, + bodyText1: { + defaultMessage: 'The Scratch Team is always looking to better understand how Scratch is used around the ' + + 'world. To help support this effort, you can allow Scratch to automatically send usage information to ' + + 'the Scratch Team.', + description: 'First paragraph of body text for telemetry opt-in modal', + id: 'gui.telemetryOptIn.body1' + }, + bodyText2: { + defaultMessage: 'The information we collect includes language selection, blocks usage, and some events like ' + + 'saving, loading, and uploading a project. We DO NOT collect any personal information. Please see our ' + + '{privacyPolicyLink} for more information.', + description: 'First paragraph of body text for telemetry opt-in modal', + id: 'gui.telemetryOptIn.body2' + }, + privacyPolicyLink: { + defaultMessage: 'Privacy Policy', + description: 'Link to the Scratch privacy policy', + id: 'gui.telemetryOptIn.privacyPolicyLink' + }, + noButton: { + defaultMessage: 'No, thanks', + description: 'Text for telemetry modal opt-out button', + id: 'gui.telemetryOptIn.buttonTextNo' + }, + noTooltip: { + defaultMessage: 'Disable telemetry', + description: 'Tooltip for telemetry modal opt-out button', + id: 'gui.telemetryOptIn.buttonTooltipNo' + }, + yesButton: { + defaultMessage: "Yes, I'd like to help improve Scratch", + description: 'Text for telemetry modal opt-in button', + id: 'gui.telemetryOptIn.buttonTextYes' + }, + yesTooltip: { + defaultMessage: 'Enable telemetry', + description: 'Tooltip for telemetry modal opt-in button', + id: 'gui.telemetryOptIn.buttonTooltipYes' + } +}); + +class TelemetryModal extends React.PureComponent { + constructor (props) { + super(props); + bindAll(this, [ + 'handleCancel', + 'handleOptIn', + 'handleOptOut' + ]); + } + handleCancel () { + this.props.onRequestClose(); + if (this.props.onCancel) { + this.props.onCancel(); + } + } + handleOptIn () { + this.props.onRequestClose(); + if (this.props.onOptIn) { + this.props.onOptIn(); + } + } + handleOptOut () { + this.props.onRequestClose(); + if (this.props.onOptOut) { + this.props.onOptOut(); + } + } + render () { + return ( +
+ + + +

+

+ + ) + }} + />

+ + + + +
+
+
); + } +} + +TelemetryModal.propTypes = { + intl: intlShape.isRequired, + isRtl: PropTypes.bool, + onCancel: PropTypes.func, + onOptIn: PropTypes.func.isRequired, + onOptOut: PropTypes.func.isRequired, + onRequestClose: PropTypes.func +}; + +export default injectIntl(TelemetryModal); diff --git a/src/components/waveform/waveform.jsx b/src/components/waveform/waveform.jsx index 1b2abe96e..48a0a4935 100644 --- a/src/components/waveform/waveform.jsx +++ b/src/components/waveform/waveform.jsx @@ -2,52 +2,65 @@ import React from 'react'; import PropTypes from 'prop-types'; import styles from './waveform.css'; -const Waveform = props => { - const { - width, - height, - data - } = props; +// Waveform is expensive to compute, make sure it only updates when data does +// by using PureComponent. In future can be changed back to function with React.memo +// eslint-disable-next-line react/prefer-stateless-function +class Waveform extends React.PureComponent { + render () { + const { + width, + height, + data + } = this.props; - const cappedData = [0, ...data, 0]; + // Never want a density of points higher than the number of pixels + // This is very conservative, could be far fewer points because of curve smoothing. + // Drawing too many points seems to cause an explosion in browser + // composite time when animating the playhead + const takeEveryN = Math.ceil(data.length / width); - const points = [ - ...cappedData.map((v, i) => - [width * i / cappedData.length, height * v / 2] - ), - ...cappedData.reverse().map((v, i) => - [width * (cappedData.length - i - 1) / cappedData.length, -height * v / 2] - ) - ]; + const filteredData = takeEveryN === 1 ? data : + data.filter((_, i) => i % takeEveryN === 0); - const pathComponents = points.map(([x, y], i) => { - const [nx, ny] = points[i < points.length - 1 ? i + 1 : 0]; - return `Q${x} ${y} ${(x + nx) / 2} ${(y + ny) / 2}`; - }); + const cappedData = [0, ...filteredData, 0]; - return ( - - - - + [width * i / cappedData.length, height * v / 2] + ), + ...cappedData.reverse().map((v, i) => + [width * (cappedData.length - i - 1) / cappedData.length, -height * v / 2] + ) + ]; + const pathComponents = points.map(([x, y], i) => { + const [nx, ny] = points[i < points.length - 1 ? i + 1 : 0]; + return `Q${x} ${y} ${(x + nx) / 2} ${(y + ny) / 2}`; + }); + + return ( + + - - - ); -}; + + + + + ); + } +} Waveform.propTypes = { data: PropTypes.arrayOf(PropTypes.number), diff --git a/src/components/browser-modal/unsupported.png b/src/components/webgl-modal/unsupported.png similarity index 100% rename from src/components/browser-modal/unsupported.png rename to src/components/webgl-modal/unsupported.png diff --git a/src/components/webgl-modal/webgl-modal.css b/src/components/webgl-modal/webgl-modal.css index 10eed71f1..85fcc0d01 100644 --- a/src/components/webgl-modal/webgl-modal.css +++ b/src/components/webgl-modal/webgl-modal.css @@ -30,7 +30,7 @@ width: 100%; height: 208px; background-color: $motion-primary; - background-image: url('../browser-modal/unsupported.png'); + background-image: url('./unsupported.png'); background-size: cover; } diff --git a/src/components/webgl-modal/webgl-modal.jsx b/src/components/webgl-modal/webgl-modal.jsx index faf5ac551..8c11014eb 100644 --- a/src/components/webgl-modal/webgl-modal.jsx +++ b/src/components/webgl-modal/webgl-modal.jsx @@ -39,7 +39,7 @@ const WebGlModal = ({intl, ...props}) => ( webGlLink: ( + {(_, downloadProject) => ( + + )} ); } } -const mapStateToProps = state => ({ - state: state -}); +const mapStateToProps = () => ({}); const mapDispatchToProps = dispatch => ({ onOpenConnectionModal: id => { dispatch(setConnectionModalExtensionId(id)); dispatch(openConnectionModal()); + }, + onSaveNow: () => { + dispatch(manualUpdateProject()); } }); Alert.propTypes = { + closeButton: PropTypes.bool, + content: PropTypes.element, extensionId: PropTypes.string, + extensionName: PropTypes.string, + iconSpinner: PropTypes.bool, iconURL: PropTypes.string, index: PropTypes.number, + level: PropTypes.string.isRequired, message: PropTypes.string, onCloseAlert: PropTypes.func.isRequired, onOpenConnectionModal: PropTypes.func, - showReconnect: PropTypes.bool.isRequired + onSaveNow: PropTypes.func, + showDownload: PropTypes.bool, + showReconnect: PropTypes.bool, + showSaveNow: PropTypes.bool }; export default connect( diff --git a/src/containers/alerts.jsx b/src/containers/alerts.jsx index 21a17700a..a417aae31 100644 --- a/src/containers/alerts.jsx +++ b/src/containers/alerts.jsx @@ -3,7 +3,8 @@ import PropTypes from 'prop-types'; import {connect} from 'react-redux'; import { - closeAlert + closeAlert, + filterPopupAlerts } from '../reducers/alerts'; import AlertsComponent from '../components/alerts/alerts.jsx'; @@ -14,7 +15,8 @@ const Alerts = ({ onCloseAlert }) => ( diff --git a/src/containers/backdrop-library.jsx b/src/containers/backdrop-library.jsx index 5a0266038..9d7a709cc 100644 --- a/src/containers/backdrop-library.jsx +++ b/src/containers/backdrop-library.jsx @@ -5,11 +5,6 @@ import {defineMessages, injectIntl, intlShape} from 'react-intl'; import {connect} from 'react-redux'; import VM from 'scratch-vm'; -import { - activateTab, - COSTUMES_TAB_INDEX -} from '../reducers/editor-tab'; - import analytics from '../lib/analytics'; import backdropLibraryContent from '../lib/libraries/backdrops.json'; import backdropTags from '../lib/libraries/backdrop-tags'; @@ -40,7 +35,6 @@ class BackdropLibrary extends React.Component { skinId: null }; this.props.vm.setEditingTarget(this.props.stageID); - this.props.onActivateTab(COSTUMES_TAB_INDEX); this.props.vm.addBackdrop(item.md5, vmBackdrop); analytics.event({ category: 'library', @@ -64,7 +58,6 @@ class BackdropLibrary extends React.Component { BackdropLibrary.propTypes = { intl: intlShape.isRequired, - onActivateTab: PropTypes.func.isRequired, onRequestClose: PropTypes.func, stageID: PropTypes.string.isRequired, vm: PropTypes.instanceOf(VM).isRequired @@ -74,9 +67,7 @@ const mapStateToProps = state => ({ stageID: state.scratchGui.targets.stage.id }); -const mapDispatchToProps = dispatch => ({ - onActivateTab: tab => dispatch(activateTab(tab)) -}); +const mapDispatchToProps = () => ({}); export default injectIntl(connect( mapStateToProps, diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 9a8e5c9b6..98a01db1b 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -7,7 +7,6 @@ import React from 'react'; import VMScratchBlocks from '../lib/blocks'; import VM from 'scratch-vm'; -import analytics from '../lib/analytics'; import log from '../lib/log.js'; import Prompt from './prompt.jsx'; import BlocksComponent from '../components/blocks/blocks.jsx'; @@ -51,6 +50,7 @@ class Blocks extends React.Component { bindAll(this, [ 'attachVM', 'detachVM', + 'getToolboxXML', 'handleCategorySelected', 'handleConnectionModalStart', 'handleDrop', @@ -95,6 +95,11 @@ class Blocks extends React.Component { ); this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig); + // Store the xml of the toolbox that is actually rendered. + // This is used in componentDidUpdate instead of prevProps, because + // the xml can change while e.g. on the costumes tab. + this._renderedToolboxXML = this.props.toolboxXML; + // we actually never want the workspace to enable "refresh toolbox" - this basically re-renders the // entire toolbox every time we reset the workspace. We call updateToolbox as a part of // componentDidUpdate so the toolbox will still correctly be updated @@ -113,14 +118,12 @@ class Blocks extends React.Component { if (this.props.isVisible) { this.setLocale(); } - - analytics.pageview('/editors/blocks'); } shouldComponentUpdate (nextProps, nextState) { return ( this.state.prompt !== nextState.prompt || this.props.isVisible !== nextProps.isVisible || - this.props.toolboxXML !== nextProps.toolboxXML || + this._renderedToolboxXML !== nextProps.toolboxXML || this.props.extensionLibraryVisible !== nextProps.extensionLibraryVisible || this.props.customProceduresVisible !== nextProps.customProceduresVisible || this.props.locale !== nextProps.locale || @@ -134,13 +137,17 @@ class Blocks extends React.Component { this.ScratchBlocks.hideChaff(); } - if (prevProps.toolboxXML !== this.props.toolboxXML) { + // Only rerender the toolbox when the blocks are visible and the xml is + // different from the previously rendered toolbox xml. + // Do not check against prevProps.toolboxXML because that may not have been rendered. + if (this.props.isVisible && this.props.toolboxXML !== this._renderedToolboxXML) { // rather than update the toolbox "sync" -- update it in the next frame clearTimeout(this.toolboxUpdateTimeout); this.toolboxUpdateTimeout = setTimeout(() => { this.updateToolbox(); }, 0); } + if (this.props.isVisible === prevProps.isVisible) { if (this.props.stageSize !== prevProps.stageSize) { // force workspace to redraw for the new stage size @@ -158,7 +165,6 @@ class Blocks extends React.Component { this.setLocale(); } else { this.props.vm.refreshWorkspace(); - this.updateToolbox(); } window.dispatchEvent(new Event('resize')); @@ -173,13 +179,16 @@ class Blocks extends React.Component { } setLocale () { - this.workspace.getFlyout().setRecyclingEnabled(false); this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); this.props.vm.setLocale(this.props.locale, this.props.messages) .then(() => { + this.workspace.getFlyout().setRecyclingEnabled(false); this.props.vm.refreshWorkspace(); - this.updateToolbox(); - this.workspace.getFlyout().setRecyclingEnabled(true); + // refreshWorkspace will cause a toolbox update + // wait for update to go through before reenabling recycling + this.withToolboxUpdates(() => { + this.workspace.getFlyout().setRecyclingEnabled(true); + }); }); } @@ -189,6 +198,8 @@ class Blocks extends React.Component { const categoryId = this.workspace.toolbox_.getSelectedCategoryId(); const offset = this.workspace.toolbox_.getCategoryScrollOffset(); this.workspace.updateToolbox(this.props.toolboxXML); + this._renderedToolboxXML = this.props.toolboxXML; + // In order to catch any changes that mutate the toolbox during "normal runtime" // (variable changes/etc), re-enable toolbox refresh. // Using the setter function will rerender the entire toolbox which we just rendered. @@ -233,7 +244,7 @@ class Blocks extends React.Component { this.props.vm.addListener('EXTENSION_ADDED', this.handleExtensionAdded); this.props.vm.addListener('BLOCKSINFO_UPDATE', this.handleBlocksInfoUpdate); this.props.vm.addListener('PERIPHERAL_CONNECTED', this.handleStatusButtonUpdate); - this.props.vm.addListener('PERIPHERAL_DISCONNECT_ERROR', this.handleStatusButtonUpdate); + this.props.vm.addListener('PERIPHERAL_DISCONNECTED', this.handleStatusButtonUpdate); } detachVM () { this.props.vm.removeListener('SCRIPT_GLOW_ON', this.onScriptGlowOn); @@ -246,7 +257,7 @@ class Blocks extends React.Component { this.props.vm.removeListener('EXTENSION_ADDED', this.handleExtensionAdded); this.props.vm.removeListener('BLOCKSINFO_UPDATE', this.handleBlocksInfoUpdate); this.props.vm.removeListener('PERIPHERAL_CONNECTED', this.handleStatusButtonUpdate); - this.props.vm.removeListener('PERIPHERAL_DISCONNECT_ERROR', this.handleStatusButtonUpdate); + this.props.vm.removeListener('PERIPHERAL_DISCONNECTED', this.handleStatusButtonUpdate); } updateToolboxBlockValue (id, value) { @@ -262,7 +273,7 @@ class Blocks extends React.Component { } onTargetsUpdate () { - if (this.props.vm.editingTarget) { + if (this.props.vm.editingTarget && this.workspace.getFlyout()) { ['glide', 'move', 'set'].forEach(prefix => { this.updateToolboxBlockValue(`${prefix}x`, Math.round(this.props.vm.editingTarget.x).toString()); this.updateToolboxBlockValue(`${prefix}y`, Math.round(this.props.vm.editingTarget.y).toString()); @@ -297,12 +308,32 @@ class Blocks extends React.Component { onVisualReport (data) { this.workspace.reportValue(data.id, data.value); } + getToolboxXML () { + // Use try/catch because this requires digging pretty deep into the VM + // Code inside intentionally ignores several error situations (no stage, etc.) + // Because they would get caught by this try/catch + try { + let {editingTarget: target, runtime} = this.props.vm; + const stage = runtime.getTargetForStage(); + if (!target) target = stage; // If no editingTarget, use the stage + + const stageCostumes = stage.getCostumes(); + const targetCostumes = target.getCostumes(); + const targetSounds = target.getSounds(); + const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML(); + return makeToolboxXML(target.isStage, target.id, dynamicBlocksXML, + targetCostumes[0].name, + stageCostumes[0].name, + targetSounds.length > 0 ? targetSounds[0].name : '' + ); + } catch { + return null; + } + } onWorkspaceUpdate (data) { // When we change sprites, update the toolbox to have the new sprite's blocks - if (this.props.vm.editingTarget) { - const target = this.props.vm.editingTarget; - const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML(); - const toolboxXML = makeToolboxXML(target.isStage, target.id, dynamicBlocksXML); + const toolboxXML = this.getToolboxXML(); + if (toolboxXML) { this.props.updateToolboxState(toolboxXML); } @@ -325,7 +356,9 @@ class Blocks extends React.Component { // incomplete. Throwing the error would keep things like setting the // correct editing target from happening which can interfere with // some blocks and processes in the vm. - error.message = `Workspace Update Error: ${error.message}`; + if (error.message) { + error.message = `Workspace Update Error: ${error.message}`; + } log.error(error); } this.workspace.addChangeListener(this.props.vm.blockListener); @@ -348,12 +381,9 @@ class Blocks extends React.Component { // this actually defines blocks and MUST run regardless of the UI state this.ScratchBlocks.defineBlocksWithJsonArray(blocksInfo.map(blockInfo => blockInfo.json).filter(x => x)); - // update the toolbox view: this can be skipped if we're not looking at a target, etc. - const runtime = this.props.vm.runtime; - const target = runtime.getEditingTarget() || runtime.getTargetForStage(); - if (target) { - const dynamicBlocksXML = runtime.getBlocksXML(); - const toolboxXML = makeToolboxXML(target.isStage, target.id, dynamicBlocksXML); + // Update the toolbox with new blocks + const toolboxXML = this.getToolboxXML(); + if (toolboxXML) { this.props.updateToolboxState(toolboxXML); } } @@ -380,10 +410,11 @@ class Blocks extends React.Component { this.ScratchBlocks.Msg.VARIABLE_MODAL_TITLE; p.prompt.varType = typeof optVarType === 'string' ? optVarType : this.ScratchBlocks.SCALAR_VARIABLE_TYPE; - p.prompt.showMoreOptions = + p.prompt.showVariableOptions = // This flag means that we should show variable/list options about scope optVarType !== this.ScratchBlocks.BROADCAST_MESSAGE_VARIABLE_TYPE && p.prompt.title !== this.ScratchBlocks.Msg.RENAME_VARIABLE_MODAL_TITLE && p.prompt.title !== this.ScratchBlocks.Msg.RENAME_LIST_MODAL_TITLE; + p.prompt.showCloudOption = (optVarType === this.ScratchBlocks.SCALAR_VARIABLE_TYPE) && this.props.canUseCloud; this.setState(p); } handleConnectionModalStart (extensionId) { @@ -395,11 +426,17 @@ class Blocks extends React.Component { handleOpenSoundRecorder () { this.props.onOpenSoundRecorder(); } - handlePromptCallback (input, optionSelection) { + + /* + * Pass along information about proposed name and variable options (scope and isCloud) + * and additional potentially conflicting variable names from the VM + * to the variable validation prompt callback used in scratch-blocks. + */ + handlePromptCallback (input, variableOptions) { this.state.prompt.callback( input, this.props.vm.runtime.getAllVarNamesOfType(this.state.prompt.varType), - optionSelection); + variableOptions); this.handlePromptClose(); } handlePromptClose () { @@ -423,6 +460,7 @@ class Blocks extends React.Component { /* eslint-disable no-unused-vars */ const { anyModalVisible, + canUseCloud, customProceduresVisible, extensionLibraryVisible, options, @@ -450,11 +488,13 @@ class Blocks extends React.Component { /> {this.state.prompt ? ( @@ -481,6 +521,7 @@ class Blocks extends React.Component { Blocks.propTypes = { anyModalVisible: PropTypes.bool, + canUseCloud: PropTypes.bool, customProceduresVisible: PropTypes.bool, extensionLibraryVisible: PropTypes.bool, isRtl: PropTypes.bool, diff --git a/src/containers/controls.jsx b/src/containers/controls.jsx index 2869a047a..db052b494 100644 --- a/src/containers/controls.jsx +++ b/src/containers/controls.jsx @@ -20,6 +20,9 @@ class Controls extends React.Component { if (e.shiftKey) { this.props.vm.setTurboMode(!this.props.turbo); } else { + if (!this.props.isStarted) { + this.props.vm.start(); + } this.props.vm.greenFlag(); analytics.event({ category: 'general', @@ -38,6 +41,7 @@ class Controls extends React.Component { render () { const { vm, // eslint-disable-line no-unused-vars + isStarted, // eslint-disable-line no-unused-vars projectRunning, turbo, ...props @@ -55,12 +59,14 @@ class Controls extends React.Component { } Controls.propTypes = { + isStarted: PropTypes.bool.isRequired, projectRunning: PropTypes.bool.isRequired, turbo: PropTypes.bool.isRequired, vm: PropTypes.instanceOf(VM) }; const mapStateToProps = state => ({ + isStarted: state.scratchGui.vmStatus.running, projectRunning: state.scratchGui.vmStatus.running, turbo: state.scratchGui.vmStatus.turbo }); diff --git a/src/containers/costume-library.jsx b/src/containers/costume-library.jsx index 070c9f4b9..aafe18bc3 100644 --- a/src/containers/costume-library.jsx +++ b/src/containers/costume-library.jsx @@ -37,7 +37,7 @@ class CostumeLibrary extends React.PureComponent { bitmapResolution: item.info.length > 2 ? item.info[2] : 1, skinId: null }; - this.props.vm.addCostume(item.md5, vmCostume); + this.props.vm.addCostumeFromLibrary(item.md5, vmCostume); analytics.event({ category: 'library', action: 'Select Costume', diff --git a/src/containers/costume-tab.jsx b/src/containers/costume-tab.jsx index 321c25c9a..5df459150 100644 --- a/src/containers/costume-tab.jsx +++ b/src/containers/costume-tab.jsx @@ -150,8 +150,12 @@ class CostumeTab extends React.Component { handleDuplicateCostume (costumeIndex) { this.props.vm.duplicateCostume(costumeIndex); } - handleNewCostume (costume) { - this.props.vm.addCostume(costume.md5, costume); + handleNewCostume (costume, fromCostumeLibrary) { + if (fromCostumeLibrary) { + this.props.vm.addCostumeFromLibrary(costume.md5, costume); + } else { + this.props.vm.addCostume(costume.md5, costume); + } } handleNewBlankCostume () { const name = this.props.vm.editingTarget.isStage ? @@ -173,7 +177,7 @@ class CostumeTab extends React.Component { bitmapResolution: item.info.length > 2 ? item.info[2] : 1, skinId: null }; - this.handleNewCostume(vmCostume); + this.handleNewCostume(vmCostume, true /* fromCostumeLibrary */); } handleSurpriseBackdrop () { const item = backdropLibraryContent[Math.floor(Math.random() * backdropLibraryContent.length)]; diff --git a/src/containers/error-boundary.jsx b/src/containers/error-boundary.jsx index 6485a41b8..0b7704ba2 100644 --- a/src/containers/error-boundary.jsx +++ b/src/containers/error-boundary.jsx @@ -1,18 +1,17 @@ import React from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; -import bowser from 'bowser'; import BrowserModalComponent from '../components/browser-modal/browser-modal.jsx'; import CrashMessageComponent from '../components/crash-message/crash-message.jsx'; import log from '../lib/log.js'; -import supportedBrowser from '../lib/supported-browser'; -import analytics from '../lib/analytics'; +import {recommendedBrowser} from '../lib/supported-browser'; class ErrorBoundary extends React.Component { constructor (props) { super(props); this.state = { - hasError: false + hasError: false, + errorId: null }; } @@ -23,24 +22,23 @@ class ErrorBoundary extends React.Component { message: 'Unknown error' }; - // Display fallback UI - this.setState({hasError: true}); - - // Log errors to analytics, separating supported browsers from unsupported. - if (supportedBrowser()) { - analytics.event({ - category: 'error', - action: this.props.action, - label: error.message - }); - } else { - analytics.event({ - category: 'Unsupported Browser Error', - action: `(Unsupported Browser) ${this.props.action}`, - label: `${bowser.name} ${error.message}` + // Log errors to analytics, leaving out browsers that are not in our recommended set + if (recommendedBrowser() && window.Sentry) { + window.Sentry.withScope(scope => { + Object.keys(info).forEach(key => { + scope.setExtra(key, info[key]); + }); + scope.setExtra('action', this.props.action); + window.Sentry.captureException(error); }); } + // Display fallback UI + this.setState({ + hasError: true, + errorId: window.Sentry ? window.Sentry.lastEventId() : null + }); + // Log error locally for debugging as well. log.error(`Unhandled Error: ${error.stack}\nComponent stack: ${info.componentStack}`); } @@ -55,10 +53,16 @@ class ErrorBoundary extends React.Component { render () { if (this.state.hasError) { - if (supportedBrowser()) { - return ; + if (recommendedBrowser()) { + return ( + + ); } return (); diff --git a/src/containers/green-flag-overlay.jsx b/src/containers/green-flag-overlay.jsx new file mode 100644 index 000000000..d6dcd44cc --- /dev/null +++ b/src/containers/green-flag-overlay.jsx @@ -0,0 +1,60 @@ +import bindAll from 'lodash.bindall'; +import React from 'react'; +import PropTypes from 'prop-types'; + +import {connect} from 'react-redux'; +import VM from 'scratch-vm'; +import Box from '../components/box/box.jsx'; +import greenFlag from '../components/green-flag/icon--green-flag.svg'; + +class GreenFlagOverlay extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleClick' + ]); + } + + handleClick () { + this.props.vm.start(); + this.props.vm.greenFlag(); + } + + render () { + if (this.props.isStarted) return null; + + return ( + +
+ +
+
+ + ); + } +} + +GreenFlagOverlay.propTypes = { + className: PropTypes.string, + isStarted: PropTypes.bool, + vm: PropTypes.instanceOf(VM), + wrapperClass: PropTypes.string +}; + +const mapStateToProps = state => ({ + isStarted: state.scratchGui.vmStatus.started, + vm: state.scratchGui.vm +}); + +const mapDispatchToProps = () => ({}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(GreenFlagOverlay); diff --git a/src/containers/green-flag.jsx b/src/containers/green-flag.jsx deleted file mode 100644 index a725b7e1b..000000000 --- a/src/containers/green-flag.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import bindAll from 'lodash.bindall'; -import PropTypes from 'prop-types'; -import React from 'react'; - -import VM from 'scratch-vm'; - -import GreenFlagComponent from '../components/green-flag/green-flag.jsx'; - -class GreenFlag extends React.Component { - constructor (props) { - super(props); - bindAll(this, [ - 'handleClick', - 'onProjectRunStart', - 'onProjectRunStop' - ]); - this.state = {projectRunning: false}; - } - componentDidMount () { - this.props.vm.addListener('PROJECT_RUN_START', this.onProjectRunStart); - this.props.vm.addListener('PROJECT_RUN_STOP', this.onProjectRunStop); - } - componentWillUnmount () { - this.props.vm.removeListener('PROJECT_RUN_START', this.onProjectRunStart); - this.props.vm.removeListener('PROJECT_RUN_STOP', this.onProjectRunStop); - } - onProjectRunStart () { - this.setState({projectRunning: true}); - } - onProjectRunStop () { - this.setState({projectRunning: false}); - } - handleClick (e) { - e.preventDefault(); - if (e.shiftKey) { - this.props.vm.setTurboMode(!this.props.vm.runtime.turboMode); - } else { - this.props.vm.greenFlag(); - } - } - render () { - const { - vm, // eslint-disable-line no-unused-vars - ...props - } = this.props; - return ( - - ); - } -} - -GreenFlag.propTypes = { - vm: PropTypes.instanceOf(VM) -}; - -export default GreenFlag; diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index 88344203a..93447e644 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -7,7 +7,6 @@ import VM from 'scratch-vm'; import {defineMessages, injectIntl, intlShape} from 'react-intl'; import ErrorBoundaryHOC from '../lib/error-boundary-hoc.jsx'; -import {openExtensionLibrary} from '../reducers/modals'; import { getIsError, getIsShowingProject @@ -22,18 +21,23 @@ import { import { closeCostumeLibrary, - closeBackdropLibrary + closeBackdropLibrary, + closeTelemetryModal, + openExtensionLibrary } from '../reducers/modals'; import FontLoaderHOC from '../lib/font-loader-hoc.jsx'; import LocalizationHOC from '../lib/localization-hoc.jsx'; import ProjectFetcherHOC from '../lib/project-fetcher-hoc.jsx'; import ProjectSaverHOC from '../lib/project-saver-hoc.jsx'; +import QueryParserHOC from '../lib/query-parser-hoc.jsx'; import storage from '../lib/storage'; import vmListenerHOC from '../lib/vm-listener-hoc.jsx'; import vmManagerHOC from '../lib/vm-manager-hoc.jsx'; +import cloudManagerHOC from '../lib/cloud-manager-hoc.jsx'; import GUIComponent from '../components/gui/gui.jsx'; +import {setIsScratchDesktop} from '../lib/isScratchDesktop.js'; const messages = defineMessages({ defaultProjectTitle: { @@ -45,6 +49,7 @@ const messages = defineMessages({ class GUI extends React.Component { componentDidMount () { + setIsScratchDesktop(this.props.isScratchDesktop); this.setReduxTitle(this.props.projectTitle); this.props.onStorageInit(storage); } @@ -55,6 +60,11 @@ class GUI extends React.Component { if (this.props.projectTitle !== prevProps.projectTitle) { this.setReduxTitle(this.props.projectTitle); } + if (this.props.isShowingProject && !prevProps.isShowingProject) { + // this only notifies container when a project changes from not yet loaded to loaded + // At this time the project view in www doesn't need to know when a project is unloaded + this.props.onProjectLoaded(); + } } setReduxTitle (newTitle) { if (newTitle === null || typeof newTitle === 'undefined') { @@ -75,9 +85,10 @@ class GUI extends React.Component { assetHost, cloudHost, error, - hideIntro, isError, + isScratchDesktop, isShowingProject, + onProjectLoaded, onStorageInit, onUpdateProjectId, onUpdateReduxProjectTitle, @@ -108,13 +119,14 @@ GUI.propTypes = { cloudHost: PropTypes.string, error: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), fetchingProject: PropTypes.bool, - hideIntro: PropTypes.bool, importInfoVisible: PropTypes.bool, intl: intlShape, isError: PropTypes.bool, isLoading: PropTypes.bool, + isScratchDesktop: PropTypes.bool, isShowingProject: PropTypes.bool, loadingStateVisible: PropTypes.bool, + onProjectLoaded: PropTypes.func, onSeeCommunity: PropTypes.func, onStorageInit: PropTypes.func, onUpdateProjectId: PropTypes.func, @@ -124,15 +136,18 @@ GUI.propTypes = { projectHost: PropTypes.string, projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), projectTitle: PropTypes.string, + telemetryModalVisible: PropTypes.bool, vm: PropTypes.instanceOf(VM).isRequired }; GUI.defaultProps = { + isScratchDesktop: false, onStorageInit: storageInstance => storageInstance.addOfficialScratchWebStores(), + onProjectLoaded: () => {}, onUpdateProjectId: () => {} }; -const mapStateToProps = (state, ownProps) => { +const mapStateToProps = state => { const loadingState = state.scratchGui.projectState.loadingState; return { activeTabIndex: state.scratchGui.editorTab.activeTabIndex, @@ -146,17 +161,19 @@ const mapStateToProps = (state, ownProps) => { error: state.scratchGui.projectState.error, importInfoVisible: state.scratchGui.modals.importInfo, isError: getIsError(loadingState), + isFullScreen: state.scratchGui.mode.isFullScreen, isPlayerOnly: state.scratchGui.mode.isPlayerOnly, isRtl: state.locales.isRtl, isShowingProject: getIsShowingProject(loadingState), loadingStateVisible: state.scratchGui.modals.loadingProject, - previewInfoVisible: state.scratchGui.modals.previewInfo && !ownProps.hideIntro, + previewInfoVisible: state.scratchGui.modals.previewInfo, projectId: state.scratchGui.projectState.projectId, + soundsTabVisible: state.scratchGui.editorTab.activeTabIndex === SOUNDS_TAB_INDEX, targetIsStage: ( state.scratchGui.targets.stage && state.scratchGui.targets.stage.id === state.scratchGui.targets.editingTarget ), - soundsTabVisible: state.scratchGui.editorTab.activeTabIndex === SOUNDS_TAB_INDEX, + telemetryModalVisible: state.scratchGui.modals.telemetryModal, tipsLibraryVisible: state.scratchGui.modals.tipsLibrary, vm: state.scratchGui.vm }; @@ -169,6 +186,7 @@ const mapDispatchToProps = dispatch => ({ onActivateSoundsTab: () => dispatch(activateTab(SOUNDS_TAB_INDEX)), onRequestCloseBackdropLibrary: () => dispatch(closeBackdropLibrary()), onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary()), + onRequestCloseTelemetryModal: () => dispatch(closeTelemetryModal()), onUpdateReduxProjectTitle: title => dispatch(setProjectTitle(title)) }); @@ -184,10 +202,12 @@ const WrappedGui = compose( LocalizationHOC, ErrorBoundaryHOC('Top Level App'), FontLoaderHOC, + QueryParserHOC, ProjectFetcherHOC, ProjectSaverHOC, vmListenerHOC, - vmManagerHOC + vmManagerHOC, + cloudManagerHOC )(ConnectedGUI); WrappedGui.setAppElement = ReactModal.setAppElement; diff --git a/src/containers/inline-messages.jsx b/src/containers/inline-messages.jsx new file mode 100644 index 000000000..c47943e09 --- /dev/null +++ b/src/containers/inline-messages.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {connect} from 'react-redux'; + +import { + filterInlineAlerts +} from '../reducers/alerts'; + +import InlineMessageComponent from '../components/alerts/inline-message.jsx'; + +const InlineMessages = ({ + alertsList, + className +}) => { + if (!alertsList) { + return null; + } + // only display inline alerts here + const inlineAlerts = filterInlineAlerts(alertsList); + if (!inlineAlerts || !inlineAlerts.length) { + return null; + } + + // get first alert + const firstInlineAlert = inlineAlerts[0]; + const { + content, + iconSpinner, + level + } = firstInlineAlert; + + return ( + + ); +}; + +InlineMessages.propTypes = { + alertsList: PropTypes.arrayOf(PropTypes.object), + className: PropTypes.string +}; + +const mapStateToProps = state => ({ + alertsList: state.scratchGui.alerts.alertsList +}); + +const mapDispatchToProps = () => ({}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(InlineMessages); diff --git a/src/containers/library-item.jsx b/src/containers/library-item.jsx new file mode 100644 index 000000000..6177b916f --- /dev/null +++ b/src/containers/library-item.jsx @@ -0,0 +1,149 @@ +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {injectIntl} from 'react-intl'; + +import LibraryItemComponent from '../components/library-item/library-item.jsx'; + +class LibraryItem extends React.PureComponent { + constructor (props) { + super(props); + bindAll(this, [ + 'handleBlur', + 'handleClick', + 'handleFocus', + 'handleKeyPress', + 'handleMouseEnter', + 'handleMouseLeave', + 'rotateIcon', + 'startRotatingIcons', + 'stopRotatingIcons' + ]); + this.state = { + iconIndex: 0, + isRotatingIcon: false + }; + } + componentWillUnmount () { + clearInterval(this.intervalId); + } + handleBlur (id) { + this.handleMouseLeave(id); + } + handleClick (e) { + if (!this.props.disabled) { + this.props.onSelect(this.props.id); + } + e.preventDefault(); + } + handleFocus (id) { + this.handleMouseEnter(id); + } + handleKeyPress (e) { + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault(); + this.props.onSelect(this.props.id); + } + } + handleMouseEnter () { + this.props.onMouseEnter(this.props.id); + if (this.props.icons && this.props.icons.length) { + this.stopRotatingIcons(); + this.setState({ + isRotatingIcon: true + }, this.startRotatingIcons); + } + } + handleMouseLeave () { + this.props.onMouseLeave(this.props.id); + if (this.props.icons && this.props.icons.length) { + this.setState({ + isRotatingIcon: false + }, this.stopRotatingIcons); + } + } + startRotatingIcons () { + this.rotateIcon(); + this.intervalId = setInterval(this.rotateIcon, 300); + } + stopRotatingIcons () { + if (this.intervalId) { + this.intervalId = clearInterval(this.intervalId); + } + } + rotateIcon () { + const nextIconIndex = (this.state.iconIndex + 1) % this.props.icons.length; + this.setState({iconIndex: nextIconIndex}); + } + curIconMd5 () { + if (this.props.icons && + this.state.isRotatingIcon && + this.state.iconIndex < this.props.icons.length && + this.props.icons[this.state.iconIndex] && + this.props.icons[this.state.iconIndex].baseLayerMD5) { + return this.props.icons[this.state.iconIndex].baseLayerMD5; + } + return this.props.iconMd5; + } + render () { + const iconMd5 = this.curIconMd5(); + const iconURL = iconMd5 ? + `https://cdn.assets.scratch.mit.edu/internalapi/asset/${iconMd5}/get/` : + this.props.iconRawURL; + return ( +
+ ) + }} + /> + ), + closeButton: true, + level: AlertLevels.SUCCESS, + maxDisplaySecs: 15 + } +]; + +export { + alerts as default, + AlertLevels, + AlertTypes +}; diff --git a/src/lib/analytics.js b/src/lib/analytics.js index 7b3098d6b..a411ed958 100644 --- a/src/lib/analytics.js +++ b/src/lib/analytics.js @@ -1,6 +1,6 @@ import GoogleAnalytics from 'react-ga'; -GoogleAnalytics.initialize(process.env.GA_ID, { +GoogleAnalytics.initialize(process.env.GA_ID || window.GA_ID, { debug: (process.env.NODE_ENV !== 'production'), titleCase: true, sampleRate: (process.env.NODE_ENV === 'production') ? 100 : 0, diff --git a/src/lib/app-state-hoc.jsx b/src/lib/app-state-hoc.jsx index c8e70f25e..eacf2eab6 100644 --- a/src/lib/app-state-hoc.jsx +++ b/src/lib/app-state-hoc.jsx @@ -10,7 +10,6 @@ import {setPlayer, setFullScreen} from '../reducers/mode.js'; import locales from 'scratch-l10n'; import {detectLocale} from './detect-locale'; -import {detectTutorialId} from './tutorial-from-url'; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; @@ -52,8 +51,8 @@ const AppStateHOC = function (WrappedComponent, localesOnly) { guiMiddleware, initFullScreen, initPlayer, - initTutorialCard, - initTutorialLibrary + initPreviewInfo, + initTelemetryModal } = guiRedux; const {ScratchPaintReducer} = require('scratch-paint'); @@ -65,18 +64,10 @@ const AppStateHOC = function (WrappedComponent, localesOnly) { if (props.isPlayerOnly) { initializedGui = initPlayer(initializedGui); } - } else { - const tutorialId = detectTutorialId(); - if (tutorialId !== null) { - // When loading a tutorial from the URL, - // load w/o preview modal - // open requested tutorial card or tutorials library modal for 'all' - if (tutorialId === 'all') { - initializedGui = initTutorialLibrary(initializedGui); - } else { - initializedGui = initTutorialCard(initializedGui, tutorialId); - } - } + } else if (props.showTelemetryModal) { + initializedGui = initTelemetryModal(initializedGui); + } else if (props.showPreviewInfo) { + initializedGui = initPreviewInfo(initializedGui); } reducers = { locales: localesReducer, @@ -109,12 +100,16 @@ const AppStateHOC = function (WrappedComponent, localesOnly) { const { isFullScreen, // eslint-disable-line no-unused-vars isPlayerOnly, // eslint-disable-line no-unused-vars + showPreviewInfo, // eslint-disable-line no-unused-vars + showTelemetryModal, // eslint-disable-line no-unused-vars ...componentProps } = this.props; return ( - + ); @@ -122,7 +117,8 @@ const AppStateHOC = function (WrappedComponent, localesOnly) { } AppStateWrapper.propTypes = { isFullScreen: PropTypes.bool, - isPlayerOnly: PropTypes.bool + isPlayerOnly: PropTypes.bool, + showPreviewInfo: PropTypes.bool }; return AppStateWrapper; }; diff --git a/src/lib/assets/icon--success.svg b/src/lib/assets/icon--success.svg new file mode 100644 index 000000000..d506e13b0 --- /dev/null +++ b/src/lib/assets/icon--success.svg @@ -0,0 +1,9 @@ + + + + Alerts/Check + Created with Sketch. + + + + \ No newline at end of file diff --git a/src/lib/audio/audio-recorder.js b/src/lib/audio/audio-recorder.js index 2d1bcb91d..b452b1f26 100644 --- a/src/lib/audio/audio-recorder.js +++ b/src/lib/audio/audio-recorder.js @@ -96,8 +96,14 @@ class AudioRecorder { } } - const trimStart = Math.max(2, firstChunkAboveThreshold - 2) / this.buffers.length; - const trimEnd = Math.min(this.buffers.length - 2, lastChunkAboveThreshold + 2) / this.buffers.length; + let trimStart = Math.max(2, firstChunkAboveThreshold - 2) / this.buffers.length; + let trimEnd = Math.min(this.buffers.length - 2, lastChunkAboveThreshold + 2) / this.buffers.length; + + // With very few samples, the automatic trimming can produce invalid values + if (trimStart >= trimEnd) { + trimStart = 0; + trimEnd = 1; + } const buffer = new Float32Array(this.buffers.length * this.bufferLength); diff --git a/src/lib/audio/audio-util.js b/src/lib/audio/audio-util.js index a493fc7da..2b68fcbe8 100644 --- a/src/lib/audio/audio-util.js +++ b/src/lib/audio/audio-util.js @@ -1,3 +1,5 @@ +const SOUND_BYTE_LIMIT = 10 * 1000 * 1000; // 10mb + const computeRMS = function (samples, scaling = 0.55) { if (samples.length === 0) return 0; // Calculate RMS, adapted from https://github.com/Tonejs/Tone.js/blob/master/Tone/component/Meter.js#L88 @@ -23,5 +25,6 @@ const computeChunkedRMS = function (samples, chunkSize = 1024) { export { computeRMS, - computeChunkedRMS + computeChunkedRMS, + SOUND_BYTE_LIMIT }; diff --git a/src/lib/backpack/costume-payload.js b/src/lib/backpack/costume-payload.js index 7b5e699ca..9681f2b24 100644 --- a/src/lib/backpack/costume-payload.js +++ b/src/lib/backpack/costume-payload.js @@ -1,4 +1,5 @@ import jpegThumbnail from './jpeg-thumbnail'; +import getCostumeUrl from '../get-costume-url'; const costumePayload = costume => { // TODO is it ok to base64 encode SVGs? What about unicode text inside them? @@ -26,8 +27,10 @@ const costumePayload = costume => { alert(`Cannot serialize for format: ${assetDataFormat}`); // eslint-disable-line } - // TODO we may be able to optimize by not actually generating thumbnails... - return jpegThumbnail(assetDataUrl).then(thumbnail => { + // Do not generate the thumbnail from the raw asset. Instead use the getCostumeUrl + // utility which inlines the fonts to make the thumbnail show the right fonts. + const inlinedFontDataUrl = getCostumeUrl(costume.asset); + return jpegThumbnail(inlinedFontDataUrl).then(thumbnail => { payload.thumbnail = thumbnail.replace('data:image/jpeg;base64,', ''); return payload; }); diff --git a/src/lib/backpack/sound-payload.js b/src/lib/backpack/sound-payload.js index b1694fcff..111f700b8 100644 --- a/src/lib/backpack/sound-payload.js +++ b/src/lib/backpack/sound-payload.js @@ -18,6 +18,13 @@ const soundPayload = sound => { payload.mime = 'audio/x-wav'; payload.body = assetDataUrl.replace('data:audio/x-wav;base64,', ''); break; + case 'mp3': + payload.mime = 'audio/mp3'; + // TODO scratch-storage should be fixed so that encodeDataURI does not + // always prepend the wave format header; Once that is fixed, the following + // line will have to change. + payload.body = assetDataUrl.replace('data:audio/x-wav;base64,', ''); + break; default: alert(`Cannot serialize for format: ${assetDataFormat}`); // eslint-disable-line } diff --git a/src/lib/backpack/sprite-payload.js b/src/lib/backpack/sprite-payload.js index 12fc68c40..6343dc407 100644 --- a/src/lib/backpack/sprite-payload.js +++ b/src/lib/backpack/sprite-payload.js @@ -1,23 +1,29 @@ import jpegThumbnail from './jpeg-thumbnail'; -const spritePayload = (sprite, vm) => vm.exportSprite( - sprite.id, - 'base64' -).then(zippedSprite => { - const payload = { - type: 'sprite', - name: sprite.name, - mime: 'application/zip', - body: zippedSprite, - // Filled in below - thumbnail: '' - }; +const spritePayload = (id, vm) => { + const target = vm.runtime.getTargetById(id); + if (!target) return null; - const costumeDataUrl = sprite.costume.asset.encodeDataURI(); - return jpegThumbnail(costumeDataUrl).then(thumbnail => { - payload.thumbnail = thumbnail.replace('data:image/jpeg;base64,', ''); - return payload; + return vm.exportSprite( + id, + 'base64' + ).then(zippedSprite => { + const payload = { + type: 'sprite', + name: target.sprite.name, + mime: 'application/zip', + body: zippedSprite, + // Filled in below + thumbnail: '' + }; + + const costumeDataUrl = target.sprite.costumes[target.currentCostume].asset.encodeDataURI(); + + return jpegThumbnail(costumeDataUrl).then(thumbnail => { + payload.thumbnail = thumbnail.replace('data:image/jpeg;base64,', ''); + return payload; + }); }); -}); +}; export default spritePayload; diff --git a/src/lib/blocks.js b/src/lib/blocks.js index 212831d0a..eadf6d6ff 100644 --- a/src/lib/blocks.js +++ b/src/lib/blocks.js @@ -47,6 +47,32 @@ export default function (vm) { }; }; + + const jsonForSensingMenus = function (menuOptionsFn) { + return { + message0: ScratchBlocks.Msg.SENSING_OF, + args0: [ + { + type: 'field_dropdown', + name: 'PROPERTY', + options: function () { + return menuOptionsFn(); + } + + }, + { + type: 'input_value', + name: 'OBJECT' + } + ], + output: true, + colour: ScratchBlocks.Colours.sensing.primary, + colourSecondary: ScratchBlocks.Colours.sensing.secondary, + colourTertiary: ScratchBlocks.Colours.sensing.tertiary, + outputShape: ScratchBlocks.OUTPUT_SHAPE_ROUND + }; + }; + const soundsMenu = function () { let menu = [['', '']]; if (vm.editingTarget && vm.editingTarget.sprite.sounds.length > 0) { @@ -185,6 +211,71 @@ export default function (vm) { this.jsonInit(json); }; + ScratchBlocks.Blocks.sensing_of.init = function () { + const blockId = this.id; + // Function that fills in menu for the first input in the sensing block. + // Called every time it opens since it depends on the values in the other block input. + const menuFn = function () { + const stageOptions = [ + [ScratchBlocks.Msg.SENSING_OF_BACKDROPNUMBER, 'backdrop #'], + [ScratchBlocks.Msg.SENSING_OF_BACKDROPNAME, 'backdrop name'], + [ScratchBlocks.Msg.SENSING_OF_VOLUME, 'volume'] + ]; + const spriteOptions = [ + [ScratchBlocks.Msg.SENSING_OF_XPOSITION, 'x position'], + [ScratchBlocks.Msg.SENSING_OF_YPOSITION, 'y position'], + [ScratchBlocks.Msg.SENSING_OF_DIRECTION, 'direction'], + [ScratchBlocks.Msg.SENSING_OF_COSTUMENUMBER, 'costume #'], + [ScratchBlocks.Msg.SENSING_OF_COSTUMENAME, 'costume name'], + [ScratchBlocks.Msg.SENSING_OF_SIZE, 'size'], + [ScratchBlocks.Msg.SENSING_OF_VOLUME, 'volume'] + ]; + if (vm.editingTarget) { + let lookupBlocks = vm.editingTarget.blocks; + let sensingOfBlock = lookupBlocks.getBlock(blockId); + + // The block doesn't exist, but should be in the flyout. Look there. + if (!sensingOfBlock) { + sensingOfBlock = vm.runtime.flyoutBlocks.getBlock(blockId); + // If we still don't have a block, just return an empty list . This happens during + // scratch blocks construction. + if (!sensingOfBlock) { + return [['', '']]; + } + // The block was in the flyout so look up future block info there. + lookupBlocks = vm.runtime.flyoutBlocks; + } + // Get all the stage variables (no lists) so we can add them to menu when the stage is selected. + const stageVariableOptions = vm.runtime.getTargetForStage().getAllVariableNamesInScopeByType(''); + const stageVariableMenuItems = stageVariableOptions.map(variable => [variable, variable]); + if (sensingOfBlock.inputs.OBJECT.shadow !== sensingOfBlock.inputs.OBJECT.block) { + // There's a block dropped on top of the menu. It'd be nice to evaluate it and + // return the correct list, but that is tricky. Scratch2 just returns stage options + // so just do that here too. + return stageOptions.concat(stageVariableMenuItems); + } + const menuBlock = lookupBlocks.getBlock(sensingOfBlock.inputs.OBJECT.shadow); + const selectedItem = menuBlock.fields.OBJECT.value; + if (selectedItem === '_stage_') { + return stageOptions.concat(stageVariableMenuItems); + } + // Get all the local variables (no lists) and add them to the menu. + const target = vm.runtime.getSpriteTargetByName(selectedItem); + let spriteVariableOptions = []; + // The target should exist, but there are ways for it not to (e.g. #4203). + if (target) { + spriteVariableOptions = target.getAllVariableNamesInScopeByType('', true); + } + const spriteVariableMenuItems = spriteVariableOptions.map(variable => [variable, variable]); + return spriteOptions.concat(spriteVariableMenuItems); + } + return [['', '']]; + }; + + const json = jsonForSensingMenus(menuFn); + this.jsonInit(json); + }; + ScratchBlocks.Blocks.sensing_distancetomenu.init = function () { const mouse = ScratchBlocks.ScratchMsgs.translate('SENSING_DISTANCETO_POINTER', 'mouse-pointer'); const json = jsonForMenuBlock('DISTANCETOMENU', spriteMenu, sensingColors, [ diff --git a/src/lib/cloud-manager-hoc.jsx b/src/lib/cloud-manager-hoc.jsx new file mode 100644 index 000000000..05047b435 --- /dev/null +++ b/src/lib/cloud-manager-hoc.jsx @@ -0,0 +1,163 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {connect} from 'react-redux'; +import bindAll from 'lodash.bindall'; + +import VM from 'scratch-vm'; +import CloudProvider from '../lib/cloud-provider'; + +import { + getIsShowingWithId +} from '../reducers/project-state'; + +import { + showAlertWithTimeout +} from '../reducers/alerts'; + +/* + * Higher Order Component to manage the connection to the cloud dserver. + * @param {React.Component} WrappedComponent component to manage VM events for + * @returns {React.Component} connected component with vm events bound to redux + */ +const cloudManagerHOC = function (WrappedComponent) { + class CloudManager extends React.Component { + constructor (props) { + super(props); + this.cloudProvider = null; + bindAll(this, [ + 'handleCloudDataUpdate' + ]); + + this.props.vm.on('HAS_CLOUD_DATA_UPDATE', this.handleCloudDataUpdate); + } + componentDidMount () { + if (this.shouldConnect(this.props)) { + this.connectToCloud(); + } + } + componentDidUpdate (prevProps) { + // TODO need to add cloud provider disconnection logic and cloud data clearing logic + // when loading a new project e.g. via file upload + // (and eventually move it out of the vm.clear function) + + if (this.shouldConnect(this.props) && !this.shouldConnect(prevProps)) { + this.connectToCloud(); + } + + if (this.shouldDisconnect(this.props, prevProps)) { + this.disconnectFromCloud(); + } + } + componentWillUnmount () { + this.disconnectFromCloud(); + } + canUseCloud (props) { + return !!(props.cloudHost && props.username && props.vm && props.projectId && props.hasCloudPermission); + } + shouldNotModifyCloudData (props) { + return (props.hasEverEnteredEditor && !props.canSave); + } + shouldConnect (props) { + return !this.isConnected() && this.canUseCloud(props) && + props.isShowingWithId && props.vm.runtime.hasCloudData() && + !this.shouldNotModifyCloudData(props); + } + shouldDisconnect (props, prevProps) { + return this.isConnected() && + ( // Can no longer use cloud or cloud provider info is now stale + !this.canUseCloud(props) || + !props.vm.runtime.hasCloudData() || + (props.projectId !== prevProps.projectId) || + (props.username !== prevProps.username) || + // Editing someone else's project + this.shouldNotModifyCloudData(props) + ); + } + isConnected () { + return this.cloudProvider && !!this.cloudProvider.connection; + } + connectToCloud () { + this.cloudProvider = new CloudProvider( + this.props.cloudHost, + this.props.vm, + this.props.username, + this.props.projectId); + this.props.vm.setCloudProvider(this.cloudProvider); + } + disconnectFromCloud () { + if (this.cloudProvider) { + this.cloudProvider.requestCloseConnection(); + this.cloudProvider = null; + this.props.vm.setCloudProvider(null); + } + } + handleCloudDataUpdate (projectHasCloudData) { + if (this.isConnected() && !projectHasCloudData) { + this.disconnectFromCloud(); + } else if (this.shouldConnect(this.props)) { + this.props.onShowCloudInfo(); + this.connectToCloud(); + } + } + render () { + const { + /* eslint-disable no-unused-vars */ + cloudHost, + projectId, + username, + hasCloudPermission, + hasEverEnteredEditor, + isShowingWithId, + onShowCloudInfo, + /* eslint-enable no-unused-vars */ + vm, + ...componentProps + } = this.props; + return ( + + ); + } + } + + CloudManager.propTypes = { + canSave: PropTypes.bool.isRequired, + cloudHost: PropTypes.string, + hasCloudPermission: PropTypes.bool, + hasEverEnteredEditor: PropTypes.bool, + isShowingWithId: PropTypes.bool, + onShowCloudInfo: PropTypes.func, + projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + username: PropTypes.string, + vm: PropTypes.instanceOf(VM).isRequired + }; + + const mapStateToProps = state => { + const loadingState = state.scratchGui.projectState.loadingState; + return { + hasEverEnteredEditor: state.scratchGui.mode.hasEverEnteredEditor, + isShowingWithId: getIsShowingWithId(loadingState), + projectId: state.scratchGui.projectState.projectId + }; + }; + + const mapDispatchToProps = dispatch => ({ + onShowCloudInfo: () => showAlertWithTimeout(dispatch, 'cloudInfo') + }); + + // Allow incoming props to override redux-provided props. Used to mock in tests. + const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign( + {}, stateProps, dispatchProps, ownProps + ); + + return connect( + mapStateToProps, + mapDispatchToProps, + mergeProps + )(CloudManager); +}; + +export default cloudManagerHOC; diff --git a/src/lib/cloud-provider.js b/src/lib/cloud-provider.js index 9ca5424e3..ce26082fc 100644 --- a/src/lib/cloud-provider.js +++ b/src/lib/cloud-provider.js @@ -1,4 +1,5 @@ import log from './log.js'; +import throttle from 'lodash.throttle'; class CloudProvider { @@ -16,53 +17,105 @@ class CloudProvider { this.vm = vm; this.username = username; this.projectId = projectId; + this.cloudHost = cloudHost; - // Open a websocket connection to the clouddata server - this.openConnection(cloudHost); + this.connectionAttempts = 0; + + // A queue of messages to send which were received before the + // connection was ready + this.queuedData = []; + + this.openConnection(); + + // Send a message to the cloud server at a rate of no more + // than 10 messages/sec. + this.sendCloudData = throttle(this._sendCloudData, 100); } /** * Open a new websocket connection to the clouddata server. * @param {string} cloudHost The cloud data server to connect to. */ - openConnection (cloudHost) { - if (window.WebSocket === null) { - log.warn('Websocket support is not available in this browser'); + openConnection () { + this.connectionAttempts += 1; + + try { + this.connection = new WebSocket((location.protocol === 'http:' ? 'ws://' : 'wss://') + this.cloudHost); + } catch (e) { + log.warn('Websocket support is not available in this browser', e); this.connection = null; return; } - this.connection = new WebSocket((location.protocol === 'http:' ? 'ws://' : 'wss://') + cloudHost); + this.connection.onerror = this.onError.bind(this); + this.connection.onmessage = this.onMessage.bind(this); + this.connection.onopen = this.onOpen.bind(this); + this.connection.onclose = this.onClose.bind(this); + } - this.connection.onerror = e => { - log.error(`Websocket connection error: ${JSON.stringify(e)}`); + onError (event) { + log.error(`Websocket connection error: ${JSON.stringify(event)}`); + // Error is always followed by close, which handles reconnect logic. + } - // TODO Add re-connection attempt logic here - }; - - this.connection.onmessage = event => { - const messageString = event.data; - log.info(`Received websocket message: ${messageString}`); - const message = JSON.parse(messageString); - if (message.method === 'set') { - const varData = { - varUpdate: { - name: message.name, - value: message.value - } - }; - this.vm.postIOData('cloud', varData); + onMessage (event) { + const messageString = event.data; + // Multiple commands can be received, newline separated + messageString.split('\n').forEach(message => { + if (message) { // .split can also contain '' in the array it returns + const parsedData = this.parseMessage(JSON.parse(message)); + this.vm.postIOData('cloud', parsedData); } - }; + }); + } - this.connection.onopen = () => { - this.writeToServer('handshake'); - log.info(`Successfully connected to clouddata server.`); - }; + onOpen () { + // Reset connection attempts to 1 to make sure any subsequent reconnects + // use connectionAttempts=1 to calculate timeout + this.connectionAttempts = 1; + this.writeToServer('handshake'); + log.info(`Successfully connected to clouddata server.`); - this.connection.onclose = () => { - log.info(`Closed connection to websocket`); - }; + // Go through the queued data and send off messages that we weren't + // ready to send before + this.queuedData.forEach(data => { + this.sendCloudData(data); + }); + // Reset the queue + this.queuedData = []; + } + + onClose () { + log.info(`Closed connection to websocket`); + const randomizedTimeout = this.randomizeDuration(this.exponentialTimeout()); + this.setTimeout(this.openConnection.bind(this), randomizedTimeout); + } + + exponentialTimeout () { + return (Math.pow(2, Math.min(this.connectionAttempts, 5)) - 1) * 1000; + } + + randomizeDuration (t) { + return Math.random() * t; + } + + setTimeout (fn, time) { + log.info(`Reconnecting in ${(time / 1000).toFixed(1)}s, attempt ${this.connectionAttempts}`); + this._connectionTimeout = window.setTimeout(fn, time); + } + + parseMessage (message) { + const varData = {}; + switch (message.method) { + case 'set': { + varData.varUpdate = { + name: message.name, + value: message.value + }; + break; + } + } + return varData; } /** @@ -70,10 +123,9 @@ class CloudProvider { * @param {string} methodName The message method, indicating the action to perform. * @param {string} dataName The name of the cloud variable this message pertains to * @param {string | number} dataValue The value to set the cloud variable to - * @param {number} dataIndex The index of the item to update (for cloud lists) * @param {string} dataNewName The new name for the cloud variable (if renaming) */ - writeToServer (methodName, dataName, dataValue, dataIndex, dataNewName) { + writeToServer (methodName, dataName, dataValue, dataNewName) { const msg = {}; msg.method = methodName; msg.user = this.username; @@ -84,20 +136,35 @@ class CloudProvider { if (dataNewName) msg.new_name = dataNewName; // Optional number params need different undefined check - if (typeof dataValue !== 'undefined') msg.value = dataValue; - if (typeof dataValue !== 'undefined') msg.index = dataIndex; + if (typeof dataValue !== 'undefined' && dataValue !== null) msg.value = dataValue; const dataToWrite = JSON.stringify(msg); - this.sendCloudData(dataToWrite); + if (this.connection && this.connection.readyState === WebSocket.OPEN) { + this.sendCloudData(dataToWrite); + } else if (msg.method === 'create' || msg.method === 'delete' || msg.method === 'rename') { + // Save data for sending when connection is open, iff the data + // is a create, rename, or delete + this.queuedData.push(dataToWrite); + } + } /** * Send a formatted message to the cloud data server. * @param {string} data The formatted message to send. */ - sendCloudData (data) { + _sendCloudData (data) { this.connection.send(`${data}\n`); - log.info(`Sent message to clouddata server: ${data}`); + } + + /** + * Provides an API for the VM's cloud IO device to create + * a new cloud variable on the server. + * @param {string} name The name of the variable to create + * @param {string | number} value The value of the new cloud variable. + */ + createVariable (name, value) { + this.writeToServer('create', name, value); } /** @@ -110,12 +177,39 @@ class CloudProvider { this.writeToServer('set', name, value); } + /** + * Provides an API for the VM's cloud IO device to rename + * a cloud variable on the server. + * @param {string} oldName The old name of the variable to rename + * @param {string} newName The new name for the cloud variable. + */ + renameVariable (oldName, newName) { + this.writeToServer('rename', oldName, null, newName); + } + + /** + * Provides an API for the VM's cloud IO device to delete + * a cloud variable on the server. + * @param {string} name The name of the variable to delete + */ + deleteVariable (name) { + this.writeToServer('delete', name); + } + /** * Closes the connection to the web socket and clears the cloud * provider of references related to the cloud data project. */ requestCloseConnection () { - this.connection.close(); + if (this.connection && + this.connection.readyState !== WebSocket.CLOSING && + this.connection.readyState !== WebSocket.CLOSED) { + log.info('Request close cloud connection without reconnecting'); + // Remove listeners, after this point we do not want to react to connection updates + this.connection.onclose = () => {}; + this.connection.onerror = () => {}; + this.connection.close(); + } this.clear(); } @@ -128,6 +222,11 @@ class CloudProvider { this.vm = null; this.username = null; this.projectId = null; + if (this._connectionTimeout) { + clearTimeout(this._connectionTimeout); + this._connectionTimeout = null; + } + this.connectionAttempts = 0; } } diff --git a/src/lib/data-uri-to-blob.js b/src/lib/data-uri-to-blob.js new file mode 100644 index 000000000..d71748e6b --- /dev/null +++ b/src/lib/data-uri-to-blob.js @@ -0,0 +1,17 @@ +/** + * Utility to convert data URIs to blobs + * Adapted from https://stackoverflow.com/questions/12168909/blob-from-dataurl + * @param {string} dataURI the data uri to blobify + * @return {Blob} a blob representing the data uri + */ +export default function dataURItoBlob (dataURI) { + const byteString = atob(dataURI.split(',')[1]); + const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; + const arrayBuffer = new ArrayBuffer(byteString.length); + const uintArray = new Uint8Array(arrayBuffer); + for (let i = 0; i < byteString.length; i++) { + uintArray[i] = byteString.charCodeAt(i); + } + const blob = new Blob([arrayBuffer], {type: mimeString}); + return blob; +} diff --git a/src/lib/default-project/09dc888b0b7df19f70d81588ae73420e.svg b/src/lib/default-project/09dc888b0b7df19f70d81588ae73420e.svg index d449b3d15..cf8e01588 100755 --- a/src/lib/default-project/09dc888b0b7df19f70d81588ae73420e.svg +++ b/src/lib/default-project/09dc888b0b7df19f70d81588ae73420e.svg @@ -1,42 +1,42 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/lib/default-project/3696356a03a8d938318876a593572843.svg b/src/lib/default-project/3696356a03a8d938318876a593572843.svg index 0ecb2de81..657f5b598 100755 --- a/src/lib/default-project/3696356a03a8d938318876a593572843.svg +++ b/src/lib/default-project/3696356a03a8d938318876a593572843.svg @@ -1,37 +1,37 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/lib/default-project/project-data.js b/src/lib/default-project/project-data.js index e108eaa17..3cd500ec4 100644 --- a/src/lib/default-project/project-data.js +++ b/src/lib/default-project/project-data.js @@ -62,10 +62,7 @@ const projectData = translateFunction => { md5ext: '83a9787d4cb6f3b7632b4ddfebf74367.wav' } ], - volume: 100, - tempo: 60, - videoTransparency: 50, - videoState: 'off' + volume: 100 }, { isStage: false, @@ -77,22 +74,22 @@ const projectData = translateFunction => { currentCostume: 0, costumes: [ { - assetId: '09dc888b0b7df19f70d81588ae73420e', + assetId: 'b7853f557e4426412e64bb3da6531a99', name: translator(messages.costume, {index: 1}), bitmapResolution: 1, - md5ext: '09dc888b0b7df19f70d81588ae73420e.svg', + md5ext: 'b7853f557e4426412e64bb3da6531a99.svg', dataFormat: 'svg', - rotationCenterX: 47, - rotationCenterY: 55 + rotationCenterX: 48, + rotationCenterY: 50 }, { - assetId: '3696356a03a8d938318876a593572843', + assetId: 'e6ddc55a6ddd9cc9d84fe0b4c21e016f', name: translator(messages.costume, {index: 2}), bitmapResolution: 1, - md5ext: '3696356a03a8d938318876a593572843.svg', + md5ext: 'e6ddc55a6ddd9cc9d84fe0b4c21e016f.svg', dataFormat: 'svg', - rotationCenterX: 47, - rotationCenterY: 55 + rotationCenterX: 46, + rotationCenterY: 53 } ], sounds: [ diff --git a/src/lib/download-text.js b/src/lib/download-text.js new file mode 100644 index 000000000..2c0bc91a5 --- /dev/null +++ b/src/lib/download-text.js @@ -0,0 +1,13 @@ +export default (filename, text) => { + const pom = document.createElement('a'); + pom.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`); + pom.setAttribute('download', filename); + + if (document.createEvent) { + const event = document.createEvent('MouseEvents'); + event.initEvent('click', true, true); + pom.dispatchEvent(event); + } else { + pom.click(); + } +}; diff --git a/src/lib/file-uploader.js b/src/lib/file-uploader.js index f399bc214..12b503a20 100644 --- a/src/lib/file-uploader.js +++ b/src/lib/file-uploader.js @@ -1,5 +1,6 @@ import {BitmapAdapter} from 'scratch-svg-renderer'; import log from './log.js'; +import randomizeSpritePosition from './randomize-sprite-position.js'; /** * Extract the file name given a string of the form fileName + ext @@ -199,7 +200,7 @@ const spriteUpload = function (fileData, fileType, spriteName, storage, handleSp const newSprite = { name: spriteName, isStage: false, - x: 0, + x: 0, // x/y will be randomized below y: 0, visible: true, size: 100, @@ -212,6 +213,7 @@ const spriteUpload = function (fileData, fileType, spriteName, storage, handleSp costumes: [vmCostume], sounds: [] // TODO are all of these necessary? }; + randomizeSpritePosition(newSprite); // TODO probably just want sprite upload to handle this object directly handleSprite(JSON.stringify(newSprite)); })); diff --git a/src/lib/font-loader-hoc.jsx b/src/lib/font-loader-hoc.jsx index cbb7856c3..9e98cf127 100644 --- a/src/lib/font-loader-hoc.jsx +++ b/src/lib/font-loader-hoc.jsx @@ -1,18 +1,29 @@ import React from 'react'; +import PropTypes from 'prop-types'; +import omit from 'lodash.omit'; +import {connect} from 'react-redux'; +import {setFontsLoaded} from '../reducers/fonts-loaded'; +// This list is from scratch-render-fonts: +// https://github.com/LLK/scratch-render-fonts/blob/master/src/index.js#L4 +const FONTS = [ + 'Sans Serif', + 'Serif', + 'Handwriting', + 'Marker', + 'Curly', + 'Pixel', + 'Scratch' +]; /* Higher Order Component to provide behavior for loading fonts. * @param {React.Component} WrappedComponent component to receive fontsLoaded prop * @returns {React.Component} component with font loading behavior */ const FontLoaderHOC = function (WrappedComponent) { class FontLoaderComponent extends React.Component { - constructor (props) { - super(props); - this.state = { - fontsLoaded: false - }; - } componentDidMount () { + if (this.props.fontsLoaded) return; + const getFontPromises = () => { const fontPromises = []; // Browsers that support the font loader interface have an iterable document.fonts.values() @@ -22,8 +33,12 @@ const FontLoaderHOC = function (WrappedComponent) { typeof document.fonts.values === 'function' && typeof document.fonts.values()[Symbol.iterator] === 'function') { for (const fontFace of document.fonts.values()) { - fontPromises.push(fontFace.loaded); - fontFace.load(); + // Only load fonts from this list. If we load all fonts on the document, we may block on + // loading fonts from things like chrome extensions. + if (FONTS.indexOf(fontFace.family) !== -1) { + fontPromises.push(fontFace.loaded); + fontFace.load(); + } } } return fontPromises; @@ -32,28 +47,43 @@ const FontLoaderHOC = function (WrappedComponent) { // objects get replaced and the old ones never resolve. if (document.readyState === 'complete') { Promise.all(getFontPromises()).then(() => { - this.setState({fontsLoaded: true}); + this.props.onSetFontsLoaded(); }); } else { document.onreadystatechange = () => { if (document.readyState !== 'complete') return; document.onreadystatechange = null; Promise.all(getFontPromises()).then(() => { - this.setState({fontsLoaded: true}); + this.props.onSetFontsLoaded(); }); }; } } render () { + const componentProps = omit(this.props, ['onSetFontsLoaded']); return ( ); } } - return FontLoaderComponent; + + + FontLoaderComponent.propTypes = { + fontsLoaded: PropTypes.bool.isRequired, + onSetFontsLoaded: PropTypes.func.isRequired + }; + const mapStateToProps = state => ({ + fontsLoaded: state.scratchGui.fontsLoaded + }); + const mapDispatchToProps = dispatch => ({ + onSetFontsLoaded: () => dispatch(setFontsLoaded()) + }); + return connect( + mapStateToProps, + mapDispatchToProps + )(FontLoaderComponent); }; export { diff --git a/src/lib/get-costume-url.js b/src/lib/get-costume-url.js index 2ecb196c2..7c4b66572 100644 --- a/src/lib/get-costume-url.js +++ b/src/lib/get-costume-url.js @@ -1,5 +1,5 @@ import storage from './storage'; -import {SVGRenderer} from 'scratch-svg-renderer'; +import {inlineSvgFonts} from 'scratch-svg-renderer'; // Contains 'font-family', but doesn't only contain 'font-family="none"' const HAS_FONT_REGEXP = 'font-family(?!="none")'; @@ -21,9 +21,7 @@ const getCostumeUrl = (function () { if (asset.assetType === storage.AssetType.ImageVector) { const svgString = asset.decodeText(); if (svgString.match(HAS_FONT_REGEXP)) { - const svgRenderer = new SVGRenderer(); - svgRenderer.loadString(svgString); - const svgText = svgRenderer.toString(true /* shouldInjectFonts */); + const svgText = inlineSvgFonts(svgString); cachedUrl = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`; } else { cachedUrl = asset.encodeDataURI(); diff --git a/src/lib/hash-parser-hoc.jsx b/src/lib/hash-parser-hoc.jsx index 611f67699..b948d3d69 100644 --- a/src/lib/hash-parser-hoc.jsx +++ b/src/lib/hash-parser-hoc.jsx @@ -9,6 +9,8 @@ import { setProjectId } from '../reducers/project-state'; +import {closePreviewInfo} from '../reducers/modals'; + /* Higher Order Component to get the project id from location.hash * @param {React.Component} WrappedComponent: component to render * @returns {React.Component} component with hash parsing behavior @@ -20,9 +22,6 @@ const HashParserHOC = function (WrappedComponent) { bindAll(this, [ 'handleHashChange' ]); - this.state = { - hideIntro: false - }; } componentDidMount () { window.addEventListener('hashchange', this.handleHashChange); @@ -43,9 +42,6 @@ const HashParserHOC = function (WrappedComponent) { const hashMatch = window.location.hash.match(/#(\d+)/); const hashProjectId = hashMatch === null ? defaultProjectId : hashMatch[1]; this.props.setProjectId(hashProjectId.toString()); - if (hashProjectId !== defaultProjectId) { - this.setState({hideIntro: true}); - } } render () { const { @@ -58,7 +54,6 @@ const HashParserHOC = function (WrappedComponent) { } = this.props; return ( ); @@ -77,7 +72,12 @@ const HashParserHOC = function (WrappedComponent) { }; }; const mapDispatchToProps = dispatch => ({ - setProjectId: projectId => dispatch(setProjectId(projectId)) + setProjectId: projectId => { + dispatch(setProjectId(projectId)); + if (projectId !== defaultProjectId) { + dispatch(closePreviewInfo()); + } + } }); // Allow incoming props to override redux-provided props. Used to mock in tests. const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign( diff --git a/src/lib/import-csv.js b/src/lib/import-csv.js new file mode 100644 index 000000000..36ec7392b --- /dev/null +++ b/src/lib/import-csv.js @@ -0,0 +1,23 @@ +import Papa from 'papaparse'; + +export default () => new Promise((resolve, reject) => { + const fileInput = document.createElement('input'); + fileInput.setAttribute('type', 'file'); + fileInput.setAttribute('accept', '.csv, .tsv, .txt'); // parser auto-detects delimiter + fileInput.onchange = e => { + const file = e.target.files[0]; + Papa.parse(file, { + header: false, + complete: results => { + document.body.removeChild(fileInput); + resolve(results.data); + }, + error: err => { + document.body.removeChild(fileInput); + reject(err); + } + }); + }; + document.body.appendChild(fileInput); + fileInput.click(); +}); diff --git a/src/lib/isScratchDesktop.js b/src/lib/isScratchDesktop.js new file mode 100644 index 000000000..e159e3852 --- /dev/null +++ b/src/lib/isScratchDesktop.js @@ -0,0 +1,35 @@ +/** + * Internal stored state. Not valid until after at least one call to `setIsScratchDesktop()`. + * @type {boolean} + */ +let _isScratchDesktop; // undefined = not ready yet + +/** + * Tell the `isScratchDesktop()` whether or not the GUI is running under Scratch Desktop. + * @param {boolean} value - the new value which `isScratchDesktop()` should return in the future. + */ +const setIsScratchDesktop = function (value) { + _isScratchDesktop = value; +}; + +/** + * @returns {boolean} - true if it seems like the GUI is running under Scratch Desktop; false otherwise. + * If `setIsScratchDesktop()` has not yet been called, this can return `undefined`. + */ +const isScratchDesktop = function () { + return _isScratchDesktop; +}; + +/** + * @returns {boolean} - false if it seems like the GUI is running under Scratch Desktop; true otherwise. + */ +const notScratchDesktop = function () { + return !isScratchDesktop(); +}; + +export default isScratchDesktop; +export { + isScratchDesktop, + notScratchDesktop, + setIsScratchDesktop +}; diff --git a/src/lib/libraries/backdrops.json b/src/lib/libraries/backdrops.json index 91e9b1cf4..01c93f15a 100644 --- a/src/lib/libraries/backdrops.json +++ b/src/lib/libraries/backdrops.json @@ -107,6 +107,20 @@ 2 ] }, + { + "name": "Bedroom 1", + "md5": "7aa6bbb2ddc4c10f901e1a50aeac1c7e.png", + "type": "backdrop", + "tags": [ + "bedroom", + "indoors" + ], + "info": [ + 960, + 720, + 2 + ] + }, { "name": "Bedroom 2", "md5": "e2f8b0dbd0a65d2ad8bfc21616662a6a.png", @@ -162,6 +176,20 @@ 1 ] }, + { + "name": "Blue Sky 2 ", + "md5": "5a906c2f272b77f59f0ef207857a8b3a.svg", + "type": "backdrop", + "tags": [ + "outdoors", + "flying" + ], + "info": [ + 480, + 360, + 1 + ] + }, { "name": "Boardwalk", "md5": "de0e54cd11551566f044e7e6bc588b2c.png", @@ -283,6 +311,23 @@ 2 ] }, + { + "name": "Colorful City", + "md5": "04d18ddd1b85f0ea30beb14b8da49f60.png", + "type": "backdrop", + "tags": [ + "city", + "town", + "road", + "skyline", + "outdoors" + ], + "info": [ + 960, + 720, + 2 + ] + }, { "name": "Concert", "md5": "c8d90320d2966c08af8cdd1c6a7a93b5.png", @@ -298,6 +343,22 @@ 2 ] }, + { + "name": "Desert", + "md5": "d98a9526a34890cf4bad11b5409eae2a.png", + "type": "backdrop", + "tags": [ + "desert", + " landscape", + " outdoors", + " cacti" + ], + "info": [ + 960, + 720, + 2 + ] + }, { "name": "Farm", "md5": "1e8a70bd07f1dcba3383883f3b948266.png", @@ -343,6 +404,20 @@ 2 ] }, + { + "name": "Forest", + "md5": "92968ac16b2f0c3f7835a6dacd172c7b.png", + "type": "backdrop", + "tags": [ + "outdoors", + "forest" + ], + "info": [ + 960, + 720, + 2 + ] + }, { "name": "Galaxy", "md5": "5fab1922f254ae9fd150162c3e392bef.png", @@ -358,6 +433,21 @@ 2 ] }, + { + "name": "Garden-rock", + "md5": "4f66053598bea0905e1559ab9d5a6e31.png", + "type": "backdrop", + "tags": [ + "outdoors", + " garden", + " park" + ], + "info": [ + 960, + 720, + 2 + ] + }, { "name": "Greek Theater", "md5": "93d71e8b8a96cc007b8d68f36acd338a.png", @@ -876,7 +966,7 @@ ] }, { - "name": "Stage", + "name": "Spotlight", "md5": "d26bf4c3980163d9106625cc2ea6c50d.png", "type": "backdrop", "tags": [ @@ -1100,7 +1190,7 @@ }, { "name": "Woods", - "md5": "a28d7218f148340a5184afd7210d5e62.svg", + "md5": "d26cfd278999c9e37d6af75c296a58df.svg", "type": "backdrop", "tags": [ "fantasy", diff --git a/src/lib/libraries/costumes.json b/src/lib/libraries/costumes.json index 95bab89bb..dfafb94ec 100644 --- a/src/lib/libraries/costumes.json +++ b/src/lib/libraries/costumes.json @@ -46,7 +46,7 @@ }, { "name": "Abby-d", - "md5": "2193cf08f74b8354f7c4fac37a06ea24.svg", + "md5": "2d05d4da65e7b5009a491d8153d37253.svg", "type": "costume", "tags": [ "people", @@ -54,8 +54,8 @@ "drawing" ], "info": [ - 31, - 100, + 32, + 101, 1 ] }, @@ -75,7 +75,7 @@ }, { "name": "Andie-a", - "md5": "936abdaaac3a3f7d27e5f30b3da00e5f.svg", + "md5": "419ff5a76adbb4b076c072f68267fdeb.svg", "type": "costume", "tags": [ "sports", @@ -88,13 +88,13 @@ ], "info": [ 86, - 77, + 81, 1 ] }, { "name": "Andie-b", - "md5": "cc2953492b40e231fdee3d8c9994a646.svg", + "md5": "a6a766912b4e43064ee3f389c72925b4.svg", "type": "costume", "tags": [ "sports", @@ -107,13 +107,13 @@ ], "info": [ 46, - 77, + 106, 1 ] }, { "name": "Andie-c", - "md5": "76e26ccb79a94d3e89f67e64b4cb9731.svg", + "md5": "fac1025857d2c28391cb31f39a80df54.svg", "type": "costume", "tags": [ "sports", @@ -126,13 +126,13 @@ ], "info": [ 52, - 36, + 65, 1 ] }, { "name": "Andie-d", - "md5": "80fa011244f3e3be56b96ebcb9e40d59.svg", + "md5": "7af50835e05d43e5e07cd2d641e7401e.svg", "type": "costume", "tags": [ "sports", @@ -144,8 +144,8 @@ "alex eben meyer" ], "info": [ - 86, - 43, + 87, + 72, 1 ] }, @@ -263,7 +263,7 @@ }, { "name": "Anina Stance", - "md5": "b66762519d9542c0ab4fe754b47fe593.png", + "md5": "84c5e22b4303c7c1fb707125706c9aaa.png", "type": "costume", "tags": [ "people", @@ -687,7 +687,7 @@ }, { "name": "Bananas", - "md5": "40bb8a1afe88cec78254f6fcfee98242.svg", + "md5": "1a76b68e056954f5c8ac1f073df1533d.svg", "type": "costume", "tags": [ "food", @@ -697,14 +697,14 @@ "fruit" ], "info": [ - 36, - 37, + 39, + 38, 1 ] }, { "name": "Baseball", - "md5": "ae05a2a1fc22ce5929b8a67f695bfc9c.svg", + "md5": "838dc2b2278619d3a9beefc6c8072a08.svg", "type": "costume", "tags": [ "baseball", @@ -720,7 +720,7 @@ }, { "name": "Basketball", - "md5": "b15c425f3eef68e7d095ee91321cb52a.svg", + "md5": "482f04cbffa835c4abf29ab996d3fbbd.svg", "type": "costume", "tags": [ "sports", @@ -728,14 +728,14 @@ "alex eben meyer" ], "info": [ - 26, - 26, + 23, + 23, 1 ] }, { "name": "Bat-a", - "md5": "3b6274510488d5b26447c1c266475801.svg", + "md5": "75bf9a5c5ec7dc9325a0301b6f6e72dd.svg", "type": "costume", "tags": [ "fantasy", @@ -746,14 +746,14 @@ "alex eben meyer" ], "info": [ - 65, - 61, + 80, + 60, 1 ] }, { "name": "Bat-b", - "md5": "8b20987d450379a445cbfc41ff2a3874.svg", + "md5": "b92d4967a0c233b0e78d6e834653f846.svg", "type": "costume", "tags": [ "fantasy", @@ -764,14 +764,14 @@ "alex eben meyer" ], "info": [ - 65, + 39, 61, 1 ] }, { "name": "Bat-c", - "md5": "c6d6a6892638e489902fbba803050de1.svg", + "md5": "56b1458bd81b8ac2401c216f7352343c.svg", "type": "costume", "tags": [ "fantasy", @@ -782,14 +782,14 @@ "alex eben meyer" ], "info": [ - 65, - 61, + 68, + 66, 1 ] }, { "name": "Bat-d", - "md5": "e3da64838b2c74b6dd9a080b030e327e.svg", + "md5": "7f806f928a192aab0b0d03aa96a89a67.svg", "type": "costume", "tags": [ "fantasy", @@ -800,14 +800,14 @@ "alex eben meyer" ], "info": [ - 65, - 61, + 29, + 62, 1 ] }, { "name": "Batter-a", - "md5": "cc7510ea0a0e2f6eaf232f8ca73e3b53.svg", + "md5": "f7357c4cef5f31c6460e4f69dd0adabb.svg", "type": "costume", "tags": [ "baseball", @@ -816,14 +816,14 @@ "alex eben meyer" ], "info": [ - 44, - 71, + 46, + 80, 1 ] }, { "name": "Batter-b", - "md5": "7e7f9ce18fd84e0e95a96ad30fb58c8c.svg", + "md5": "c60fe1da0dcddedfd152384c60cbd5cc.svg", "type": "costume", "tags": [ "baseball", @@ -832,14 +832,14 @@ "alex eben meyer" ], "info": [ - 14, - 58, + 16, + 67, 1 ] }, { "name": "Batter-c", - "md5": "05cc730290919069b8d08447214c7293.svg", + "md5": "165c30e123e785d4cbbc0d9621f1c349.svg", "type": "costume", "tags": [ "baseball", @@ -848,14 +848,14 @@ "alex eben meyer" ], "info": [ - 92, - 57, + 94, + 66, 1 ] }, { "name": "Batter-d", - "md5": "24c32bf9bf066e36e49beb03ccd3db8f.svg", + "md5": "e82529d9c31b56a3dd098ccc94be68a6.svg", "type": "costume", "tags": [ "baseball", @@ -864,8 +864,8 @@ "alex eben meyer" ], "info": [ - 64, - 95, + 70, + 102, 1 ] }, @@ -902,7 +902,7 @@ }, { "name": "Bear-b", - "md5": "d6517131718621a7aae8fc4f27de1ded.svg", + "md5": "92a64ce184c8515217e0e98e49624622.svg", "type": "costume", "tags": [ "animals", @@ -910,8 +910,8 @@ "walking" ], "info": [ - 80, - 140, + 94, + 191, 1 ] }, @@ -1069,7 +1069,7 @@ }, { "name": "Ben-a", - "md5": "7f32d8d78ad64f50c018b7b578de2e18.svg", + "md5": "4386d05d5abd6e1aa0a93b7b447d1b39.svg", "type": "costume", "tags": [ "sports", @@ -1079,14 +1079,14 @@ "alex eben meyer" ], "info": [ - 76, - 75, + 53, + 69, 1 ] }, { "name": "Ben-b", - "md5": "fb012e5d1baf80d33ae95fba3511151a.svg", + "md5": "90db137cd0743204d02fdc9be1c8eb94.svg", "type": "costume", "tags": [ "sports", @@ -1096,14 +1096,14 @@ "alex eben meyer" ], "info": [ - 76, - 75, + 53, + 67, 1 ] }, { "name": "Ben-c", - "md5": "be0b1397965cf8ff2c4cecb84795138a.svg", + "md5": "5f05d41ede773b3bb379286e86a5b869.svg", "type": "costume", "tags": [ "sports", @@ -1113,14 +1113,14 @@ "alex eben meyer" ], "info": [ - 76, - 75, + 71, + 72, 1 ] }, { "name": "Ben-d", - "md5": "e2aefdb538ebbb24e1ab1464f75ef134.svg", + "md5": "6f6e246567c0639c758ed4f6930be76f.svg", "type": "costume", "tags": [ "sports", @@ -1130,8 +1130,8 @@ "alex eben meyer" ], "info": [ - 76, - 75, + 42, + 72, 1 ] }, @@ -1150,41 +1150,15 @@ ] }, { - "name": "Bowtie-01", - "md5": "aebd82272a5f4a1dd3c23d6e98537017.svg", + "name": "Bowtie", + "md5": "7d876f3ba4b04f3193a701ed30b4490c.svg", "type": "costume", "tags": [ "fashion" ], "info": [ - 13, - 7, - 1 - ] - }, - { - "name": "Bowtie-a", - "md5": "534d9924d2f9bfe240f041e3ce55ccf5.svg", - "type": "costume", - "tags": [ - "fashion" - ], - "info": [ - 11, - 4, - 1 - ] - }, - { - "name": "Bowtie-b", - "md5": "8be1e90e19cd1faced8a2e83c2b5c90d.svg", - "type": "costume", - "tags": [ - "fashion" - ], - "info": [ - 11, - 4, + 15, + 8, 1 ] }, @@ -1668,7 +1642,7 @@ }, { "name": "Casey-a", - "md5": "30a4dafa852311b2a07d72e1bb060326.svg", + "md5": "b7269d70079e96299666cc7b3f8eabc3.svg", "type": "costume", "tags": [ "sports", @@ -1677,14 +1651,14 @@ "alex eben meyer" ], "info": [ - 82, - 72, + 75, + 62, 1 ] }, { "name": "Casey-b", - "md5": "c8c0a25bebac8b35b8eae7ddd716d061.svg", + "md5": "d22f6e818106f247f91e1c7c315863c3.svg", "type": "costume", "tags": [ "sports", @@ -1693,14 +1667,14 @@ "alex eben meyer" ], "info": [ - 82, - 72, + 60, + 74, 1 ] }, { "name": "Casey-c", - "md5": "104d363c48c373384c6c80abbbbb0ad6.svg", + "md5": "a6ea02408a3f8366dfd704aae9a6ed0b.svg", "type": "costume", "tags": [ "sports", @@ -1709,14 +1683,14 @@ "alex eben meyer" ], "info": [ - 61, - 61, + 57, + 72, 1 ] }, { "name": "Casey-d", - "md5": "3d4bddb908bf912b938c111bfa38c28a.svg", + "md5": "033ef301acfdfbc38774a14a559c1654.svg", "type": "costume", "tags": [ "sports", @@ -1725,8 +1699,8 @@ "alex eben meyer" ], "info": [ - 82, - 72, + 71, + 74, 1 ] }, @@ -1804,7 +1778,7 @@ }, { "name": "Cat Flying-a", - "md5": "1e81725d2d2c7de4a2dd4a145198a43c.svg", + "md5": "bf6b9f91e6928be67eda5154ca5fb2c6.svg", "type": "costume", "tags": [ "animals", @@ -1813,31 +1787,30 @@ "kitten" ], "info": [ - 67, - 48, + 55, + 37, 1 ] }, { "name": "Cat Flying-b", - "md5": "0d192725870ef0eda50d91cab0e3c9c5.svg", + "md5": "6a5f707419e619463135f5bae599fa5b.svg", "type": "costume", "tags": [ "animals", - "flying", "cat", "kitty", "kitten" ], "info": [ - 42, 44, + 46, 1 ] }, { "name": "Cat-a", - "md5": "09dc888b0b7df19f70d81588ae73420e.svg", + "md5": "b7853f557e4426412e64bb3da6531a99.svg", "type": "costume", "tags": [ "animals", @@ -1849,14 +1822,14 @@ "scratch cat" ], "info": [ - 47, - 55, + 48, + 50, 1 ] }, { "name": "Cat-b", - "md5": "3696356a03a8d938318876a593572843.svg", + "md5": "e6ddc55a6ddd9cc9d84fe0b4c21e016f.svg", "type": "costume", "tags": [ "animals", @@ -1868,14 +1841,14 @@ "scratch cat" ], "info": [ - 47, - 55, + 46, + 53, 1 ] }, { "name": "Catcher-a", - "md5": "6700cf1d4d653283161d2bad20f74d39.svg", + "md5": "27c88c54a59b9fff7e3e4763a6e9874e.svg", "type": "costume", "tags": [ "baseball", @@ -1884,14 +1857,14 @@ "alex eben meyer" ], "info": [ - 34, - 33, + 41, + 51, 1 ] }, { "name": "Catcher-b", - "md5": "d59c293e6fb4e15aa6bcfca5b1d0f2a6.svg", + "md5": "799a822cffc9460c0bf1b387cbd5e06b.svg", "type": "costume", "tags": [ "baseball", @@ -1900,14 +1873,14 @@ "alex eben meyer" ], "info": [ - 34, - 29, + 41, + 47, 1 ] }, { "name": "Catcher-c", - "md5": "55a2b54e5e414c9e2a17caf73ad06b87.svg", + "md5": "a6f2bf8b354e8d1a789f8e6f83133d02.svg", "type": "costume", "tags": [ "baseball", @@ -1916,14 +1889,14 @@ "alex eben meyer" ], "info": [ - 53, - 69, + 60, + 87, 1 ] }, { "name": "Catcher-d", - "md5": "d8943c5c2700e49f26f77826b9a3c238.svg", + "md5": "c86af45d9168bfb81259e5bb40be1778.svg", "type": "costume", "tags": [ "baseball", @@ -1932,8 +1905,8 @@ "alex eben meyer" ], "info": [ - 79, - 28, + 86, + 46, 1 ] }, @@ -2003,7 +1976,7 @@ }, { "name": "Champ99-a", - "md5": "1cb9d1a98cebc8a1d83af7633fdb9cd0.png", + "md5": "7b073f47fbd9421e0d60daacc157f506.png", "type": "costume", "tags": [ "people", @@ -2114,7 +2087,7 @@ }, { "name": "Chick-a", - "md5": "0abf71fb210bbda7eac0bb6ecaab25de.svg", + "md5": "7cd74ad0da97a828e8499ee181c9dbb1.svg", "type": "costume", "tags": [ "animals", @@ -2123,14 +2096,30 @@ "owen davey" ], "info": [ - 20, - 21, + 32, + 37, 1 ] }, { "name": "Chick-b", - "md5": "3577aafabcfd6bcc7f7512be940a18b7.svg", + "md5": "07c2dad5f7063fddf7705f4817fad4f6.svg", + "type": "costume", + "tags": [ + "animals", + "chicken", + "farm", + "owen davey" + ], + "info": [ + 32, + 37, + 1 + ] + }, + { + "name": "Chick-c", + "md5": "272d3e4503a4123df62f25b67bad6f03.svg", "type": "costume", "tags": [ "animals", @@ -2139,8 +2128,40 @@ "owen davey" ], "info": [ - 20, 21, + 24, + 1 + ] + }, + { + "name": "City Bus-a", + "md5": "accf64aceae7997b7533fb51624ca297.svg", + "type": "costume", + "tags": [ + "city", + "bus", + "vehicle", + "car" + ], + "info": [ + 107, + 67, + 1 + ] + }, + { + "name": "City Bus-b", + "md5": "b9ea0e326ffa98bc58a73bdf17c9542b.svg", + "type": "costume", + "tags": [ + "city", + "bus", + "vehicle", + "car" + ], + "info": [ + 107, + 67, 1 ] }, @@ -2238,11 +2259,12 @@ ] }, { - "name": "Convertible3", + "name": "Convertible 3", "md5": "b6ac3c9e1789cba2302d2ef82d62d019.svg", "type": "costume", "tags": [ "transportation", + "car", "things" ], "info": [ @@ -2353,43 +2375,40 @@ }, { "name": "Dani-a", - "md5": "f3038fb0f4a00806b02081c6789dd8cf.svg", + "md5": "e620d1c3143b5ff05a0e4df89576e054.svg", "type": "costume", "tags": [ - "people", - "fashion" + "people" ], "info": [ - 49, - 115, + 37, + 141, 1 ] }, { "name": "Dani-b", - "md5": "e5c7dd4905a78e1d54087b7165dd1e8c.svg", + "md5": "4de54672f1c7af00623a0ee3bc490ba7.svg", "type": "costume", "tags": [ - "people", - "fashion" + "people" ], "info": [ - 82, - 115, + 86, + 141, 1 ] }, { "name": "Dani-c", - "md5": "cbc5f9c67022af201d498bc9b35608b8.svg", + "md5": "b4a303342f9b7ed6a74059054347dd30.svg", "type": "costume", "tags": [ - "people", - "fashion" + "people" ], "info": [ - 49, - 113, + 37, + 138, 1 ] }, @@ -2512,7 +2531,7 @@ }, { "name": "Dinosaur1-a", - "md5": "75d367961807fff8e81f556da81dec24.svg", + "md5": "e70ab4de006f3df64e776abf912a62be.svg", "type": "costume", "tags": [ "animals", @@ -2528,7 +2547,7 @@ }, { "name": "Dinosaur1-b", - "md5": "ecdaee9c08ae68fd7a67f81302f00a97.svg", + "md5": "af2d8211a65dce9871d49fa70671ce0c.svg", "type": "costume", "tags": [ "animals", @@ -2544,7 +2563,7 @@ }, { "name": "Dinosaur1-c", - "md5": "02078a81abd2e10cb62ebcc853a40c92.svg", + "md5": "a2bb28d5ba55ba997bec63772437cecf.svg", "type": "costume", "tags": [ "animals", @@ -2560,7 +2579,7 @@ }, { "name": "Dinosaur1-d", - "md5": "c9ed031bc9bf11416142880f89436be9.svg", + "md5": "91f3dba45e8cec7bdff31ed5acde2c85.svg", "type": "costume", "tags": [ "animals", @@ -2569,14 +2588,14 @@ "sauropod" ], "info": [ - 98, + 99, 91, 1 ] }, { "name": "Dinosaur2-a", - "md5": "5493f5deffe7aed451cd8b255740de4a.svg", + "md5": "f49719e8f2440f5d328f2604e97eb71d.svg", "type": "costume", "tags": [ "animals", @@ -2585,14 +2604,14 @@ "alex eben meyer" ], "info": [ - 115, - 72, + 63, + 67, 1 ] }, { "name": "Dinosaur2-b", - "md5": "70bba739b7df0bd08abb31026d078ee7.svg", + "md5": "1af6b3af3bf2cda862bbe01034a336bb.svg", "type": "costume", "tags": [ "animals", @@ -2601,14 +2620,14 @@ "alex eben meyer" ], "info": [ - 74, + 76, 67, 1 ] }, { "name": "Dinosaur2-c", - "md5": "4a51679d86aafcc9cee1c010fc141288.svg", + "md5": "e83b30a4517bbe155e2b4a6367c8f70b.svg", "type": "costume", "tags": [ "animals", @@ -2617,14 +2636,14 @@ "alex eben meyer" ], "info": [ - 62, + 64, 67, 1 ] }, { "name": "Dinosaur2-d", - "md5": "47053664449b24749aaf199925b19f8e.svg", + "md5": "8244f1fb159237179d1caaa69c070060.svg", "type": "costume", "tags": [ "animals", @@ -2633,14 +2652,14 @@ "alex eben meyer" ], "info": [ - 71, - 66, + 73, + 67, 1 ] }, { "name": "Dinosaur3-a", - "md5": "17636db6f607c14a03a36e18abfea86a.svg", + "md5": "c4867c44f38df5bde472844878d5beea.svg", "type": "costume", "tags": [ "animals", @@ -2650,14 +2669,14 @@ "alex eben meyer" ], "info": [ - 115, - 72, + 113, + 42, 1 ] }, { "name": "Dinosaur3-b", - "md5": "1b20afc713b04ca5d01b25d050aa35ac.svg", + "md5": "b682c1e58ea1c6bea737670f57517b6f.svg", "type": "costume", "tags": [ "animals", @@ -2667,14 +2686,14 @@ "alex eben meyer" ], "info": [ - 115, - 72, + 113, + 59, 1 ] }, { "name": "Dinosaur3-c", - "md5": "4700613077afa7c62659b3fd7d7c748e.svg", + "md5": "47a617ca48f4782efeeb60199cff801e.svg", "type": "costume", "tags": [ "animals", @@ -2684,14 +2703,14 @@ "alex eben meyer" ], "info": [ - 115, - 72, + 47, + 59, 1 ] }, { "name": "Dinosaur3-d", - "md5": "a03f110ed12b73acc9bd84037fd6ae96.svg", + "md5": "710ff3380d6872b04785db97dc0565c6.svg", "type": "costume", "tags": [ "animals", @@ -2701,14 +2720,14 @@ "alex eben meyer" ], "info": [ - 115, - 72, + 113, + 65, 1 ] }, { "name": "Dinosaur3-e", - "md5": "00e24e40535a1a621fee0f70895b2b61.svg", + "md5": "6f40025c1157f37858adf7fa85090e64.svg", "type": "costume", "tags": [ "animals", @@ -2717,14 +2736,14 @@ "alex eben meyer" ], "info": [ - 115, - 72, + 41, + 40, 1 ] }, { "name": "Dinosaur4-a", - "md5": "9da591f8a6da251c800adb12a02c43cb.svg", + "md5": "d87b5e3c45f5d234c7aa47107c788f18.svg", "type": "costume", "tags": [ "animals", @@ -2735,14 +2754,14 @@ "alex eben meyer" ], "info": [ - 59, - 54, + 60, + 52, 1 ] }, { "name": "Dinosaur4-b", - "md5": "a3028e87caeb8338f50b2c6dec9f23d2.svg", + "md5": "cfe906702bbd30bc3bb8acb53079b6b0.svg", "type": "costume", "tags": [ "animals", @@ -2753,14 +2772,14 @@ "alex eben meyer" ], "info": [ - 86, - 54, + 87, + 52, 1 ] }, { "name": "Dinosaur4-c", - "md5": "9a4bbc1b104c8112be82c252a6ecfd11.svg", + "md5": "1879b07cfe788b0258afada17aa46973.svg", "type": "costume", "tags": [ "animals", @@ -2772,13 +2791,13 @@ ], "info": [ 59, - 54, + 52, 1 ] }, { "name": "Dinosaur4-d", - "md5": "45061ff84a25723625d04f0476687633.svg", + "md5": "52c17657698ab82881ed3ccf83080684.svg", "type": "costume", "tags": [ "animals", @@ -2790,7 +2809,7 @@ ], "info": [ 89, - 90, + 88, 1 ] }, @@ -3111,7 +3130,7 @@ }, { "name": "Dorian-a", - "md5": "b042b1a5fde03dd5abbc2f3f78d11a2c.svg", + "md5": "503f1d4d0fe61d071e1dd8b5233cceb0.svg", "type": "costume", "tags": [ "sports", @@ -3120,14 +3139,14 @@ "alex eben meyer" ], "info": [ - 82, - 72, + 60, + 65, 1 ] }, { "name": "Dorian-b", - "md5": "d0b58b672606601ecfa3a406b537fa10.svg", + "md5": "d5f795ef2395bcd11ceee0b0c24c9341.svg", "type": "costume", "tags": [ "sports", @@ -3136,14 +3155,14 @@ "alex eben meyer" ], "info": [ - 82, - 72, + 60, + 65, 1 ] }, { "name": "Dorian-c", - "md5": "445e461c73a5920098764a4cbad5bfe0.svg", + "md5": "4ad4ddf4251b9b5bcb16d8d77213685f.svg", "type": "costume", "tags": [ "sports", @@ -3152,14 +3171,14 @@ "alex eben meyer" ], "info": [ - 82, - 72, + 77, + 67, 1 ] }, { "name": "Dorian-d", - "md5": "ba93ede3bbf75c0f707b0fb982bc27e8.svg", + "md5": "67fba4e187ecf42749bdd44e64f29192.svg", "type": "costume", "tags": [ "sports", @@ -3168,68 +3187,72 @@ "alex eben meyer" ], "info": [ - 82, - 72, + 77, + 53, 1 ] }, { "name": "Dot-a", - "md5": "47c975e37f9a89c01d0d4d6fd17ef847.svg", + "md5": "3b9e6830365aeadb63c21b681777b783.svg", "type": "costume", "tags": [ + "animals", "space", "dog", "wren mcdonald" ], "info": [ 52, - 69, + 67, 1 ] }, { "name": "Dot-b", - "md5": "a9b7d5f7afa0c69c4044a3f541b9ab90.svg", + "md5": "a80a07906ffbb9671dba3633223859af.svg", "type": "costume", "tags": [ + "animals", "space", "dog", "wren mcdonald" ], "info": [ 65, - 69, + 67, 1 ] }, { "name": "Dot-c", - "md5": "5b7967159c9b83b0d0ed4f051ec2c9e9.svg", + "md5": "1e1bdccbab5b566beb7f91b5ec01352f.svg", "type": "costume", "tags": [ + "animals", "space", "dog", "wren mcdonald" ], "info": [ - 49, - 70, + 51, + 69, 1 ] }, { "name": "Dot-d", - "md5": "23ffa385654304e4cac454390dde3606.svg", + "md5": "06d70d5de0e25f97c1c606e513ab019c.svg", "type": "costume", "tags": [ + "animals", "space", "dog", "wren mcdonald" ], "info": [ - 59, - 69, + 57, + 67, 1 ] }, @@ -3344,7 +3367,54 @@ ] }, { - "name": "Dress-01", + "name": "Dragonfly-a", + "md5": "87aaecd2fb73580e6f1f38c5a2e579a9.svg", + "type": "costume", + "tags": [ + "animals", + "insect", + "dragonfly", + "wetland", + "owen davey" + ], + "info": [ + 108, + 52, + 1 + ] + }, + { + "name": "Dragonfly-b", + "md5": "4e775a49bd5a7a6d38392731924f72e0.svg", + "type": "costume", + "tags": [ + "animals", + "insect", + "dragonfly", + "wetland", + "owen davey" + ], + "info": [ + 108, + 46, + 1 + ] + }, + { + "name": "Dress-a", + "md5": "fa7e0b37a324c212d0f1ce0f701a1ae0.svg", + "type": "costume", + "tags": [ + "fashion" + ], + "info": [ + 57, + 83, + 1 + ] + }, + { + "name": "Dress-b", "md5": "308b6ad8e6cfe5c2b2c1e3a1eb9cc404.svg", "type": "costume", "tags": [ @@ -3357,7 +3427,7 @@ ] }, { - "name": "Dress-02", + "name": "Dress-c", "md5": "1f7a725ba285dc97cb142d4b812db624.svg", "type": "costume", "tags": [ @@ -3369,19 +3439,6 @@ 1 ] }, - { - "name": "Dress-03", - "md5": "fa7e0b37a324c212d0f1ce0f701a1ae0.svg", - "type": "costume", - "tags": [ - "fashion" - ], - "info": [ - 57, - 83, - 1 - ] - }, { "name": "Drum-a", "md5": "dd66742bc2a3cfe5a6f9f540afd2e15c.svg", @@ -3496,32 +3553,62 @@ }, { "name": "Drum-snare-a", - "md5": "b0255be93e3c8be6ac687f4199a25c4b.svg", + "md5": "ace58710b8fa5fca8fb5bbca15e7d19c.svg", "type": "costume", "tags": [ "music", "andrew rae" ], "info": [ - 23, - 50, + 21, + 32, 1 ] }, { "name": "Drum-snare-b", - "md5": "f6d2f2a6e1055dab6262336625ddf652.svg", + "md5": "f6120839d3f73b262571add2444c443a.svg", "type": "costume", "tags": [ "music", "andrew rae" ], "info": [ - 36, + 34, + 48, + 1 + ] + }, + { + "name": "Drums Conga-a", + "md5": "b3da94523b6d3df2dd30602399599ab4.svg", + "type": "costume", + "tags": [ + "drums", + " music", + " ericr" + ], + "info": [ + 65, 66, 1 ] }, + { + "name": "Drums Conga-b", + "md5": "037703c2b65f051c12e9b22dcf93856c.svg", + "type": "costume", + "tags": [ + "drums", + " music", + " ericr" + ], + "info": [ + 66, + 94, + 1 + ] + }, { "name": "Duck", "md5": "c3baf7eedfbdac8cd1e4f1f1f779dc0c.svg", @@ -3541,20 +3628,67 @@ }, { "name": "Earth", - "md5": "814197522984a302972998b1a7f92d91.svg", + "md5": "d9df2cf4f6eb2460d0de3e4f1e05f40b.svg", "type": "costume", "tags": [ - "space" + "space", + "planet", + "earth" ], "info": [ - 72, - 72, + 41, + 40, + 1 + ] + }, + { + "name": "Easel-a", + "md5": "edc81e35c94c07dc380c43ea9b311394.svg", + "type": "costume", + "tags": [ + "bedroom", + "art", + "easel" + ], + "info": [ + 69, + 95, + 1 + ] + }, + { + "name": "Easel-b", + "md5": "cac798e8cf0364fa9a8bc52b85bda6b2.svg", + "type": "costume", + "tags": [ + "bedroom", + "art", + "easel" + ], + "info": [ + 69, + 95, + 1 + ] + }, + { + "name": "Easel-c", + "md5": "afbf42eb496694afb31f9ea69b3a1a7a.svg", + "type": "costume", + "tags": [ + "bedroom", + "art", + "easel" + ], + "info": [ + 68, + 94, 1 ] }, { "name": "Egg-a", - "md5": "bc723738dfe626c5c3bb90970d985961.svg", + "md5": "6f1a24a63d16c45425aa2eb64934a278.svg", "type": "costume", "tags": [ "food", @@ -3562,14 +3696,14 @@ "alex eben meyer" ], "info": [ - 68, - 54, + 18, + 26, 1 ] }, { "name": "Egg-b", - "md5": "83016b7ff817f99be4a454600b4a78fc.svg", + "md5": "7f1320549dd8bfd596c52aec02d898ce.svg", "type": "costume", "tags": [ "food", @@ -3577,14 +3711,14 @@ "alex eben meyer" ], "info": [ - 68, - 54, + 23, + 27, 1 ] }, { "name": "Egg-c", - "md5": "d9a44d151fbd909bdbbcf7877055af6d.svg", + "md5": "62ed6f412aae07a575faa51451009168.svg", "type": "costume", "tags": [ "food", @@ -3592,14 +3726,14 @@ "alex eben meyer" ], "info": [ - 68, - 54, + 28, + 27, 1 ] }, { "name": "Egg-d", - "md5": "c91c7f72b8523b0910a84bce7d99c37b.svg", + "md5": "0dce0615471ebf1bebc3ba8b4bebbd18.svg", "type": "costume", "tags": [ "food", @@ -3607,14 +3741,14 @@ "alex eben meyer" ], "info": [ - 68, - 54, + 19, + 27, 1 ] }, { "name": "Egg-e", - "md5": "65c15516e62596e1f72e874359fc7254.svg", + "md5": "3848cf5708b7c712bb7bb9bbcce636aa.svg", "type": "costume", "tags": [ "food", @@ -3622,8 +3756,23 @@ "alex eben meyer" ], "info": [ - 68, - 54, + 20, + 26, + 1 + ] + }, + { + "name": "Egg-f", + "md5": "0f7e63d9f23038a5012409af93c8dfad.svg", + "type": "costume", + "tags": [ + "food", + "breakfast", + "alex eben meyer" + ], + "info": [ + 31, + 26, 1 ] }, @@ -3737,17 +3886,6 @@ 1 ] }, - { - "name": "Empty", - "md5": "cd21514d0531fdffb22204e0ec5ed84a.svg", - "type": "costume", - "tags": [], - "info": [ - 0, - 0, - 1 - ] - }, { "name": "Fairy-a", "md5": "fe97687c7f1b747bb6f41c252cc5926a.svg", @@ -3896,6 +4034,89 @@ 1 ] }, + { + "name": "Fishbowl-a", + "md5": "9d1013e04ca7425f680a4b4e6dd6ede6.svg", + "type": "costume", + "tags": [ + "fishbowl", + "fish", + "animals", + "pet" + ], + "info": [ + 28, + 24, + 1 + ] + }, + { + "name": "Fishbowl-b", + "md5": "0bbfc827e8d2f3da6db82efe95563f93.svg", + "type": "costume", + "tags": [ + "fishbowl", + "fish", + "animals", + "pet" + ], + "info": [ + 28, + 24, + 1 + ] + }, + { + "name": "Food Truck-a", + "md5": "b30ec8ff043e0f1747591d564d56d7fa.svg", + "type": "costume", + "tags": [ + "city", + "truck", + "vehicle", + "food", + "car" + ], + "info": [ + 121, + 125, + 1 + ] + }, + { + "name": "Food Truck-b", + "md5": "afd51531621d2b33869b47342311b363.svg", + "type": "costume", + "tags": [ + "city", + "truck", + "vehicle", + "food", + "car" + ], + "info": [ + 121, + 101, + 1 + ] + }, + { + "name": "Food Truck-c", + "md5": "42f88485a25bf9a683d28957db9f969f.svg", + "type": "costume", + "tags": [ + "city", + "truck", + "vehicle", + "food", + "car" + ], + "info": [ + 121, + 92, + 1 + ] + }, { "name": "Football Running", "md5": "7ee31371b2eafba57cc5a78fc1a787fe.png", @@ -3939,7 +4160,7 @@ }, { "name": "Fox-a", - "md5": "fab5488e600e81565f0fc285fc7050f8.svg", + "md5": "0aef4633e8580e209020b054eb55982f.svg", "type": "costume", "tags": [ "animals", @@ -3947,14 +4168,14 @@ "robert hunter" ], "info": [ - 94, - 54, + 86, + 44, 1 ] }, { "name": "Fox-b", - "md5": "afb192ae250a74dfac18bfc52d1d6266.svg", + "md5": "ae0bb1b900e853ddc8820e0c7bd60f53.svg", "type": "costume", "tags": [ "animals", @@ -3962,14 +4183,14 @@ "robert hunter" ], "info": [ - 94, - 54, + 44, + 50, 1 ] }, { "name": "Fox-c", - "md5": "29f858d384db7998c0e5183f6a31a3b4.svg", + "md5": "3cc3761cbabdcf13dbfdf04048bfbccf.svg", "type": "costume", "tags": [ "animals", @@ -3977,17 +4198,18 @@ "robert hunter" ], "info": [ - 94, - 54, + 48, + 38, 1 ] }, { "name": "Frank-a", - "md5": "19a09a005d7199d2e681e53a761bfe11.svg", + "md5": "8ebcc9a619ae0455df9da42d39f59c1d.svg", "type": "costume", "tags": [ "fantasy", + "people", "spooky", "halloween", "frankenstein", @@ -3995,61 +4217,64 @@ "alex eben meyer" ], "info": [ - 154, - 107, + 97, + 63, 1 ] }, { "name": "Frank-b", - "md5": "a03102442ee197a39083fba24e852792.svg", + "md5": "e605b9d87181c52825ae21597c9c1e07.svg", "type": "costume", "tags": [ "fantasy", "spooky", + "people", "halloween", "frankenstein", "monster", "alex eben meyer" ], "info": [ - 154, - 107, + 105, + 63, 1 ] }, { "name": "Frank-c", - "md5": "b456d8dc4b7895d61c90dbc909d8a4c6.svg", + "md5": "a53378250f07b6dad161516a5317277e.svg", "type": "costume", "tags": [ "fantasy", "spooky", + "people", "halloween", "frankenstein", "monster", "alex eben meyer" ], "info": [ - 154, - 107, + 159, + 59, 1 ] }, { "name": "Frank-d", - "md5": "8743c01814fed4190a582ba1520e8f6a.svg", + "md5": "44b4ba838b60f14d6e7ebddf5df4d4c4.svg", "type": "costume", "tags": [ "fantasy", "spooky", + "people", "halloween", "frankenstein", "monster", "alex eben meyer" ], "info": [ - 154, + 106, 107, 1 ] @@ -4072,6 +4297,54 @@ 1 ] }, + { + "name": "Frog 2-a", + "md5": "07160d455202e95f5b4b5c842eff6788.svg", + "type": "costume", + "tags": [ + "animals", + "frog", + "wetland", + "owen davey" + ], + "info": [ + 101, + 60, + 1 + ] + }, + { + "name": "Frog 2-b", + "md5": "9663ddb4d100eb4e18702ee13eca835e.svg", + "type": "costume", + "tags": [ + "animals", + "frog", + "wetland", + "owen davey" + ], + "info": [ + 101, + 58, + 1 + ] + }, + { + "name": "Frog 2-c", + "md5": "5c2ec9de8d1695cd4b59beea45046560.svg", + "type": "costume", + "tags": [ + "animals", + "frog", + "wetland", + "owen davey" + ], + "info": [ + 96, + 87, + 1 + ] + }, { "name": "Fruit Platter", "md5": "6c3252378da3334f63eebddbed3fae91.png", @@ -4101,7 +4374,7 @@ }, { "name": "Ghost-a", - "md5": "23c317a22dec95421f2532090cc8de48.svg", + "md5": "fca9fdf3ec9aafcfbd915105fecb6ce6.svg", "type": "costume", "tags": [ "fantasy", @@ -4112,14 +4385,14 @@ "alex eben meyer" ], "info": [ - 98, - 73, + 37, + 68, 1 ] }, { "name": "Ghost-b", - "md5": "e86a742d091370950f3353e1e430cdb2.svg", + "md5": "9d6e168d234d023476324cb72430e645.svg", "type": "costume", "tags": [ "fantasy", @@ -4130,14 +4403,14 @@ "alex eben meyer" ], "info": [ - 98, - 73, + 41, + 68, 1 ] }, { "name": "Ghost-c", - "md5": "24a8b71b780bc665c3e6d37c11221137.svg", + "md5": "8296c8aa0782b53c5188dabde5fce10f.svg", "type": "costume", "tags": [ "fantasy", @@ -4148,14 +4421,14 @@ "alex eben meyer" ], "info": [ - 98, - 73, + 61, + 72, 1 ] }, { "name": "Ghost-d", - "md5": "221b794cad3d045008299e3c8440170c.svg", + "md5": "2a3c6eb1faa6b2e4403c64945d59e122.svg", "type": "costume", "tags": [ "fantasy", @@ -4166,8 +4439,8 @@ "alex eben meyer" ], "info": [ - 98, - 73, + 78, + 69, 1 ] }, @@ -4241,20 +4514,6 @@ 1 ] }, - { - "name": "Giga Walk4", - "md5": "22e4ae40919cf0fe6b4d7649d14a6e71.svg", - "type": "costume", - "tags": [ - "fantasy", - "walking" - ], - "info": [ - 73, - 110, - 1 - ] - }, { "name": "Giga-a", "md5": "93cb048a1d199f92424b9c097fa5fa38.svg", @@ -4317,7 +4576,7 @@ }, { "name": "Giraffe-a", - "md5": "497baa643718a865b0621c1607696800.svg", + "md5": "2d3955997d2b3aa8dad9389c61de384f.svg", "type": "costume", "tags": [ "animals", @@ -4325,14 +4584,14 @@ "robert hunter" ], "info": [ - 86, - 131, + 87, + 132, 1 ] }, { "name": "Giraffe-b", - "md5": "0242ae2497839f449af064b40b358b76.svg", + "md5": "b129c6016fedc7d03fb7f0a06363af28.svg", "type": "costume", "tags": [ "animals", @@ -4340,14 +4599,14 @@ "robert hunter" ], "info": [ - 86, - 131, + 82, + 132, 1 ] }, { "name": "Giraffe-c", - "md5": "d9e8f8b7d656d4a24c1f4f6c24c5e578.svg", + "md5": "1af467957df0350765ba9a10e8d85572.svg", "type": "costume", "tags": [ "animals", @@ -4356,7 +4615,7 @@ ], "info": [ 86, - 131, + 132, 1 ] }, @@ -4391,20 +4650,6 @@ 1 ] }, - { - "name": "Glasses", - "md5": "5fcf716b53f223bc86b10ab0eca3e162.svg", - "type": "costume", - "tags": [ - "things", - "fashion" - ], - "info": [ - 16, - 9, - 1 - ] - }, { "name": "Glasses-a", "md5": "f42180039c37c3b221e1e426b92e5092.svg", @@ -4463,7 +4708,7 @@ }, { "name": "Goalie-a", - "md5": "86b0610ea21fdecb99795c5e6d52768c.svg", + "md5": "dbf6a1b0402377be2cd8c75a3b4fdf53.svg", "type": "costume", "tags": [ "sports", @@ -4472,14 +4717,14 @@ "alex eben meyer" ], "info": [ - 76, - 68, + 59, + 63, 1 ] }, { "name": "Goalie-b", - "md5": "af3ef5125d187772240a1120e7eb67ac.svg", + "md5": "987d8c2bb6c26cad265a3aa339fe432e.svg", "type": "costume", "tags": [ "sports", @@ -4488,14 +4733,14 @@ "alex eben meyer" ], "info": [ - 76, - 68, + 59, + 63, 1 ] }, { "name": "Goalie-c", - "md5": "7c9377cedae11a094d2e77bed3edb884.svg", + "md5": "52609ef559a29052e72efe08d1384980.svg", "type": "costume", "tags": [ "sports", @@ -4504,14 +4749,14 @@ "alex eben meyer" ], "info": [ - 76, - 68, + 62, + 61, 1 ] }, { "name": "Goalie-d", - "md5": "bd628034d356d30b0e9b563447471290.svg", + "md5": "a4de535b4c4a7c6a65e6736491e01497.svg", "type": "costume", "tags": [ "sports", @@ -4520,14 +4765,14 @@ "alex eben meyer" ], "info": [ - 76, - 68, + 63, + 62, 1 ] }, { "name": "Goalie-e", - "md5": "b3f6c4c0be9a0f71e9486dea51e513c3.svg", + "md5": "ca55a23f3172e9fff061484e00493d2c.svg", "type": "costume", "tags": [ "sports", @@ -4536,8 +4781,8 @@ "alex eben meyer" ], "info": [ - 76, - 68, + 59, + 69, 1 ] }, @@ -4649,6 +4894,108 @@ 1 ] }, + { + "name": "Grasshopper-a", + "md5": "fcddf46c04479b07b2944ee36d0b7635.svg", + "type": "costume", + "tags": [ + "animals", + "insect", + "bug", + "wetland", + "owen davey" + ], + "info": [ + 103, + 43, + 1 + ] + }, + { + "name": "Grasshopper-b", + "md5": "40b48bcb95f06a2281ba1cad60c8c504.svg", + "type": "costume", + "tags": [ + "animals", + "insect", + "bug", + "wetland", + "owen davey" + ], + "info": [ + 114, + 118, + 1 + ] + }, + { + "name": "Grasshopper-c", + "md5": "fa9a02a4a6df86612365dc65c065d2cb.svg", + "type": "costume", + "tags": [ + "animals", + "insect", + "bug", + "wetland", + "owen davey" + ], + "info": [ + 34, + 170, + 1 + ] + }, + { + "name": "Grasshopper-d", + "md5": "f80ba703a7fb17a870ec0818add2f3b8.svg", + "type": "costume", + "tags": [ + "animals", + "insect", + "bug", + "wetland", + "owen davey" + ], + "info": [ + 70, + 100, + 1 + ] + }, + { + "name": "Grasshopper-e", + "md5": "6519eeaa65eac086198d6901cf0a5b9e.svg", + "type": "costume", + "tags": [ + "animals", + "insect", + "bug", + "wetland", + "owen davey" + ], + "info": [ + 56, + 86, + 1 + ] + }, + { + "name": "Grasshopper-f", + "md5": "8d865e6bbea03cda509d766085b883f4.svg", + "type": "costume", + "tags": [ + "animals", + "insect", + "bug", + "wetland", + "owen davey" + ], + "info": [ + 22, + 54, + 1 + ] + }, { "name": "Green Flag", "md5": "173e20ac537d2c278ed621be3db3fc87.svg", @@ -4664,7 +5011,7 @@ }, { "name": "Griffin-a", - "md5": "d2ddc25b224ad72240f92e632afc7c69.svg", + "md5": "1ceea469ec409d0a3a13bd04ea1e756c.svg", "type": "costume", "tags": [ "fantasy", @@ -4673,14 +5020,14 @@ "flying" ], "info": [ - 150, - 110, + 127, + 109, 1 ] }, { - "name": "Griffin-fly-a", - "md5": "03d75e0c7c34e8618545a5f4913db4ea.svg", + "name": "Griffin-b", + "md5": "1dd10e8e53c048ad6f95c82501801f48.svg", "type": "costume", "tags": [ "fantasy", @@ -4689,14 +5036,14 @@ "flying" ], "info": [ - 150, - 110, + 127, + 76, 1 ] }, { - "name": "Griffin-fly-b", - "md5": "ff0795d15b6f3990345f72bc483a3353.svg", + "name": "Griffin-c", + "md5": "e4d7c3c18463f15083683ce0826adcee.svg", "type": "costume", "tags": [ "fantasy", @@ -4705,8 +5052,24 @@ "flying" ], "info": [ - 150, - 110, + 127, + 109, + 1 + ] + }, + { + "name": "Griffin-d", + "md5": "54504b1d8dedf816b0bd599e768f191d.svg", + "type": "costume", + "tags": [ + "fantasy", + "animals", + "ipzy", + "flying" + ], + "info": [ + 144, + 69, 1 ] }, @@ -4841,7 +5204,7 @@ }, { "name": "Hare-a", - "md5": "75c2e6085d0eae076ff357b56d5ed5cd.svg", + "md5": "8209c43050785e3c4276f84abb52c245.svg", "type": "costume", "tags": [ "animals", @@ -4855,14 +5218,14 @@ "robert hunter" ], "info": [ - 57, - 49, + 29, + 50, 1 ] }, { "name": "Hare-b", - "md5": "fb56d1bef942196c710612906142451b.svg", + "md5": "62796e48dc8e540398e0575d79f562fe.svg", "type": "costume", "tags": [ "animals", @@ -4877,13 +5240,13 @@ ], "info": [ 57, - 49, + 35, 1 ] }, { "name": "Hare-c", - "md5": "8de391ae70e54656f777c0ed53a91e1a.svg", + "md5": "4bc5c29283dfff4b9c876cc23540a4b5.svg", "type": "costume", "tags": [ "animals", @@ -4897,27 +5260,13 @@ "robert hunter" ], "info": [ - 57, - 49, + 40, + 40, 1 ] }, { "name": "Harper-a", - "md5": "919eb332bf73df227ec5f214be952a21.svg", - "type": "costume", - "tags": [ - "people", - "fashion" - ], - "info": [ - 54, - 135, - 1 - ] - }, - { - "name": "Harper-b", "md5": "0d5979db40b0a4dcaa601fb7679ef71c.svg", "type": "costume", "tags": [ @@ -4930,6 +5279,20 @@ 1 ] }, + { + "name": "Harper-b", + "md5": "c435a1fcdf5f60a29f620fa800e391e0.svg", + "type": "costume", + "tags": [ + "people", + "fashion" + ], + "info": [ + 54, + 134, + 1 + ] + }, { "name": "Harper-c", "md5": "9c35763ec87604ff1b4848ff18e0b040.svg", @@ -4944,101 +5307,8 @@ 1 ] }, - { - "name": "Hat", - "md5": "b3beb1f52d371428d70b65a0c4c5c001.svg", - "type": "costume", - "tags": [ - "Fashion" - ], - "info": [ - 52, - 60, - 1 - ] - }, - { - "name": "Hat Beanie", - "md5": "3271da33e4108ed08a303c2244739fbf.svg", - "type": "costume", - "tags": [ - "fashion" - ], - "info": [ - 28, - 19, - 1 - ] - }, - { - "name": "Hat Party2-a", - "md5": "9b7a84fe3e50621752917e4e484a1e2f.svg", - "type": "costume", - "tags": [], - "info": [ - 75, - 75, - 1 - ] - }, - { - "name": "Hat Winter", - "md5": "6c2ee1b97f6ec2b3457a25a3975a2009.svg", - "type": "costume", - "tags": [ - "fashion", - "winter" - ], - "info": [ - 26, - 101, - 1 - ] - }, - { - "name": "Hat Wizard", - "md5": "581571e8c8f5adeb01565e12b1b77b58.svg", - "type": "costume", - "tags": [ - "fashion", - "fantasy" - ], - "info": [ - 76, - 69, - 1 - ] - }, { "name": "Hat-a", - "md5": "874462b3e079308d775629c235940d8f.svg", - "type": "costume", - "tags": [ - "fashion", - "hat" - ], - "info": [ - 39, - 22, - 1 - ] - }, - { - "name": "Hat-b", - "md5": "a8ef874bd7542fec62785eac80019417.svg", - "type": "costume", - "tags": [ - "fashion", - "hat" - ], - "info": [ - 45, - 30, - 1 - ] - }, - { - "name": "Hat-c", "md5": "52540f9dba4537f79f1ad3067d44e3f6.svg", "type": "costume", "tags": [ @@ -5052,7 +5322,21 @@ ] }, { - "name": "Hat-d", + "name": "Hat-b", + "md5": "874462b3e079308d775629c235940d8f.svg", + "type": "costume", + "tags": [ + "fashion", + "hat" + ], + "info": [ + 39, + 22, + 1 + ] + }, + { + "name": "Hat-c", "md5": "b40accecd4bd57fed8bdb96ee15e5c05.svg", "type": "costume", "tags": [ @@ -5066,24 +5350,22 @@ ] }, { - "name": "Hatchling-a", - "md5": "fb9de507b6d95e96cb0df68e401dfd8f.svg", + "name": "Hat-d", + "md5": "a8ef874bd7542fec62785eac80019417.svg", "type": "costume", "tags": [ - "animals", - "chicken", - "farm", - "owen davey" + "fashion", + "hat" ], "info": [ - 14, - 23, + 45, + 30, 1 ] }, { - "name": "Hatchling-b", - "md5": "e504303f791a65274d740a20acbac606.svg", + "name": "Hatchling-a", + "md5": "25e4da6bcd209c60690729f5c7016b83.svg", "type": "costume", "tags": [ "animals", @@ -5093,13 +5375,13 @@ ], "info": [ 15, - 35, + 23, 1 ] }, { - "name": "Hatchling-c", - "md5": "6f9a243d84fbe7ed9a49e8ef9fb7d83c.svg", + "name": "Hatchling-b", + "md5": "b0f8451e7c52cf2b94b94c4a2051398b.svg", "type": "costume", "tags": [ "animals", @@ -5108,36 +5390,38 @@ "owen davey" ], "info": [ - 31, - 36, + 16, + 38, 1 ] }, { - "name": "Headband", - "md5": "237f77cb3e41c70f714c6d2aa4d38ea1.svg", + "name": "Hatchling-c", + "md5": "5f70ae46957bf425121ac532dc6a2f34.svg", "type": "costume", "tags": [ - "fashion", - "antennae" + "animals", + "chicken", + "farm", + "owen davey" ], "info": [ - 53, - 43, + 38, + 72, 1 ] }, { "name": "Heart Code", - "md5": "497c5df9e02467202ff93096dccaf91f.svg", + "md5": "1bc73fc472df673a1089d965b537f5ac.svg", "type": "costume", "tags": [ "food", "sweethearts" ], "info": [ - 58, - 61, + 73, + 72, 1 ] }, @@ -5155,16 +5439,16 @@ ] }, { - "name": "Heart Love It", - "md5": "d448acd247f10f32bef7823cd433f928.svg", + "name": "Heart Love", + "md5": "a9d25b961eef81521c8ed38beb1456a0.svg", "type": "costume", "tags": [ "food", "sweethearts" ], "info": [ - 58, - 61, + 73, + 72, 1 ] }, @@ -5204,29 +5488,29 @@ }, { "name": "Heart Smile", - "md5": "74a8f75d139d330b715787edbbacd83d.svg", + "md5": "66b29ca063bc74d2bacfc0a06bc7b6fc.svg", "type": "costume", "tags": [ "food", "sweethearts" ], "info": [ - 58, - 61, + 73, + 72, 1 ] }, { "name": "Heart Sweet", - "md5": "a39d78d33b051e8b12a4b2a10d77b249.svg", + "md5": "9a9a8efa369379ee8538632c43a6a3d1.svg", "type": "costume", "tags": [ "food", "sweethearts" ], "info": [ - 58, - 61, + 73, + 72, 1 ] }, @@ -5312,7 +5596,7 @@ }, { "name": "Hen-a", - "md5": "795ed34983a70c8690fe6c8f4a47a935.svg", + "md5": "eb0a9890a92ef616ef8740328eeabb79.svg", "type": "costume", "tags": [ "animals", @@ -5321,14 +5605,14 @@ "owen davey" ], "info": [ - 45, - 43, + 60, + 53, 1 ] }, { "name": "Hen-b", - "md5": "26528a6f0318a2d2294ef7071409be12.svg", + "md5": "12aabf8636096f223e9297d2fd8177a4.svg", "type": "costume", "tags": [ "animals", @@ -5337,14 +5621,14 @@ "owen davey" ], "info": [ - 45, - 43, + 63, + 50, 1 ] }, { "name": "Hen-c", - "md5": "7492692df7776c905fdbd4382696afee.svg", + "md5": "af2c57d1f12272335629635e669df487.svg", "type": "costume", "tags": [ "animals", @@ -5353,14 +5637,14 @@ "owen davey" ], "info": [ - 35, - 28, + 56, + 53, 1 ] }, { "name": "Hen-d", - "md5": "4bf0cb5453273d0f75db3cd0cd45ac72.svg", + "md5": "84d0492c094ec01d73f808c92b4c3138.svg", "type": "costume", "tags": [ "animals", @@ -5369,8 +5653,8 @@ "owen davey" ], "info": [ - 45, - 43, + 51, + 77, 1 ] }, @@ -5553,7 +5837,7 @@ }, { "name": "Jamal-a", - "md5": "90f9166fe6500d0c0caad8b1964d6b74.svg", + "md5": "c7531e6c05aa488edd6ad3c369204452.svg", "type": "costume", "tags": [ "sports", @@ -5562,14 +5846,14 @@ "alex eben meyer" ], "info": [ - 82, - 105, + 55, + 49, 1 ] }, { "name": "Jamal-b", - "md5": "c3d96ef7e99440c2fa76effce1235d3f.svg", + "md5": "8b8bd6ff0fe64e7056d1e70c63b9ec9f.svg", "type": "costume", "tags": [ "sports", @@ -5578,14 +5862,14 @@ "alex eben meyer" ], "info": [ - 82, - 105, + 75, + 53, 1 ] }, { "name": "Jamal-c", - "md5": "1fb8b9ca79f2c0a327913bd647b53fe5.svg", + "md5": "c659f292333f93854be243f84b337ec5.svg", "type": "costume", "tags": [ "sports", @@ -5594,14 +5878,14 @@ "alex eben meyer" ], "info": [ - 82, - 105, + 54, + 102, 1 ] }, { "name": "Jamal-d", - "md5": "4adb87e6123161fcaf02f7ac022a5757.svg", + "md5": "9dc8cfa22793c43aee826c76d144e09b.svg", "type": "costume", "tags": [ "sports", @@ -5610,8 +5894,8 @@ "alex eben meyer" ], "info": [ - 82, - 105, + 54, + 95, 1 ] }, @@ -5646,32 +5930,6 @@ 1 ] }, - { - "name": "Jeans-a", - "md5": "4e283da8c59bcbb9803bdc0016b14c21.svg", - "type": "costume", - "tags": [ - "fashion" - ], - "info": [ - 22, - 25, - 1 - ] - }, - { - "name": "Jeans-b", - "md5": "01732aa03a48482093fbed3ea402c4a9.svg", - "type": "costume", - "tags": [ - "fashion" - ], - "info": [ - 22, - 25, - 1 - ] - }, { "name": "Jellyfish-a", "md5": "9e6563e417350af3094c2ed02b9b0bbd.svg", @@ -5920,7 +6178,7 @@ }, { "name": "Jordyn-a", - "md5": "8dd2a2abbb8e639da8576b6e72ef9e59.svg", + "md5": "8355df83cca2baa0fdf6269d1311f1ea.svg", "type": "costume", "tags": [ "sports", @@ -5930,14 +6188,14 @@ "alex eben meyer" ], "info": [ - 76, - 68, + 51, + 63, 1 ] }, { "name": "Jordyn-b", - "md5": "9665f543147961551d8dc6f612d9cc41.svg", + "md5": "2e2c43bbd7b18524fb132f1426ca3134.svg", "type": "costume", "tags": [ "sports", @@ -5947,14 +6205,14 @@ "alex eben meyer" ], "info": [ - 76, - 68, + 51, + 63, 1 ] }, { "name": "Jordyn-c", - "md5": "ec8c2286070c77ebd9dd40c96eaae3fc.svg", + "md5": "5c0d24c53805e16358a2f6f6cc4a456e.svg", "type": "costume", "tags": [ "sports", @@ -5964,14 +6222,14 @@ "alex eben meyer" ], "info": [ - 76, 68, + 61, 1 ] }, { "name": "Jordyn-d", - "md5": "1f9ed7f29800f31ce2ee53196143a3c8.svg", + "md5": "ebe2b20b99c1e1b373fd2b02be48aab9.svg", "type": "costume", "tags": [ "sports", @@ -5981,8 +6239,8 @@ "alex eben meyer" ], "info": [ - 76, - 68, + 40, + 61, 1 ] }, @@ -6180,7 +6438,7 @@ "md5": "c018a3eed966d5f92c69f2188dfd2aae.svg", "type": "costume", "tags": [ - "animal", + "animals", "insect", "arthropod", "antennae", @@ -6196,7 +6454,13 @@ "name": "Ladybug2-b", "md5": "a2bb15ace808e070a2b815502952b292.svg", "type": "costume", - "tags": [], + "tags": [ + "animals", + "insect", + "arthropod", + "antennae", + "aphids" + ], "info": [ 49, 28, @@ -6433,7 +6697,7 @@ }, { "name": "Lion-a", - "md5": "701a41dec023bc03a5ad4e35988b9a86.svg", + "md5": "75e6b2178518fa354f1ebf7ab7061ceb.svg", "type": "costume", "tags": [ "cat", @@ -6450,7 +6714,7 @@ }, { "name": "Lion-b", - "md5": "28ac215a2574eeabc2b390a14de3c152.svg", + "md5": "7304176fe4dc85758e6b77324a185ea2.svg", "type": "costume", "tags": [ "cat", @@ -6460,14 +6724,14 @@ "robert hunter" ], "info": [ - 95, + 94, 43, 1 ] }, { "name": "Lion-c", - "md5": "4dbe981721314eb631ef4f7fa920e1a4.svg", + "md5": "8411de793909216761cd19fd95418945.svg", "type": "costume", "tags": [ "cat", @@ -6484,7 +6748,7 @@ }, { "name": "Llama", - "md5": "07158eb6d62e309bb60a6bc36baf2300.svg", + "md5": "f5841f36b41c4df26f9c724d913c279b.svg", "type": "costume", "tags": [ "animals", @@ -6492,14 +6756,14 @@ "robert hunter" ], "info": [ - 100, - 100, + 72, + 95, 1 ] }, { "name": "Llama-b", - "md5": "2021eea71514bd2b23e96076750727ae.svg", + "md5": "ef3f8491668de2b751d8d7a8921c9879.svg", "type": "costume", "tags": [ "animals", @@ -6507,14 +6771,14 @@ "robert hunter" ], "info": [ - 100, - 100, + 92, + 90, 1 ] }, { "name": "Llama-c", - "md5": "7837d7247acbc4eebb793452a35aa1f5.svg", + "md5": "b0af19d2c7a7c039c060f21874816164.svg", "type": "costume", "tags": [ "animals", @@ -6522,8 +6786,8 @@ "robert hunter" ], "info": [ - 100, - 100, + 73, + 39, 1 ] }, @@ -6544,7 +6808,7 @@ }, { "name": "Max-a", - "md5": "e10cca3bdbc09d039c2f937574f7a6ea.svg", + "md5": "5cc168bddf4561e6e7f1d30845d6efbf.svg", "type": "costume", "tags": [ "sports", @@ -6553,14 +6817,14 @@ "alex eben meyer" ], "info": [ - 82, - 68, + 70, + 61, 1 ] }, { "name": "Max-b", - "md5": "6d8ee139a741cf945d600a8cef0ea2e6.svg", + "md5": "8e2ad5735e26c5544b6b1a057f4efa80.svg", "type": "costume", "tags": [ "sports", @@ -6569,14 +6833,14 @@ "alex eben meyer" ], "info": [ - 82, - 68, + 71, + 64, 1 ] }, { "name": "Max-c", - "md5": "aa66109994d27de02711f6a0ef6de9ec.svg", + "md5": "261a40ebecfe135308befd79c6c07fd5.svg", "type": "costume", "tags": [ "sports", @@ -6585,14 +6849,14 @@ "alex eben meyer" ], "info": [ - 82, - 68, + 46, + 59, 1 ] }, { "name": "Max-d", - "md5": "a0dbf509d542c7eff6d2ddfc9c9410f1.svg", + "md5": "4b6eecd411daddef6422fa3a5850cd52.svg", "type": "costume", "tags": [ "sports", @@ -6601,8 +6865,8 @@ "alex eben meyer" ], "info": [ - 82, - 68, + 37, + 59, 1 ] }, @@ -6639,8 +6903,8 @@ ] }, { - "name": "Mermaid-swim-a", - "md5": "9f973b89b68f7d8147f157cbac8af341.svg", + "name": "Mermaid-c", + "md5": "2295784bb8e6354bfa7676089235cb9f.svg", "type": "costume", "tags": [ "fantasy", @@ -6655,8 +6919,8 @@ ] }, { - "name": "Mermaid-swim-b", - "md5": "2295784bb8e6354bfa7676089235cb9f.svg", + "name": "Mermaid-d", + "md5": "9f973b89b68f7d8147f157cbac8af341.svg", "type": "costume", "tags": [ "fantasy", @@ -6700,7 +6964,7 @@ }, { "name": "Milk-a", - "md5": "e6a7964bc4ea38c79a5a31d6ddfb5ba9.svg", + "md5": "1b8ec45254b7c40d86e7a33818b77e35.svg", "type": "costume", "tags": [ "food", @@ -6708,14 +6972,14 @@ "alex eben meyer" ], "info": [ - 68, - 71, + 32, + 63, 1 ] }, { "name": "Milk-b", - "md5": "82d4c1855fe0d400433c7344fb2af3b5.svg", + "md5": "2ba802dcac09a81f9d82cc9d3b95d71b.svg", "type": "costume", "tags": [ "food", @@ -6723,14 +6987,14 @@ "alex eben meyer" ], "info": [ - 68, - 71, + 42, + 64, 1 ] }, { "name": "Milk-c", - "md5": "50afc991b6fdad4b6547ba98ecf8a6af.svg", + "md5": "287b322e939ad54691aef4876c48f422.svg", "type": "costume", "tags": [ "food", @@ -6739,13 +7003,13 @@ ], "info": [ 47, - 44, + 56, 1 ] }, { "name": "Milk-d", - "md5": "8fc7606a176149d225a541a04fa67473.svg", + "md5": "df4f5d863fa680331aa6c8f867b9fcbb.svg", "type": "costume", "tags": [ "food", @@ -6753,14 +7017,14 @@ "alex eben meyer" ], "info": [ - 68, - 71, + 45, + 64, 1 ] }, { "name": "Milk-e", - "md5": "f2373d449b1226c44436dced422c2935.svg", + "md5": "e642678d42d5e74fe49e6ceb5538724e.svg", "type": "costume", "tags": [ "food", @@ -6768,8 +7032,8 @@ "alex eben meyer" ], "info": [ - 68, - 71, + 35, + 45, 1 ] }, @@ -6896,6 +7160,70 @@ 1 ] }, + { + "name": "Motorcycle-a", + "md5": "5065a917834923efc3e0c112d5791e80.svg", + "type": "costume", + "tags": [ + "city", + "bike", + "vehicle", + "motorcycle" + ], + "info": [ + 52, + 44, + 1 + ] + }, + { + "name": "Motorcycle-b", + "md5": "fe0923cb08c0b7c373de1daf8b74de74.svg", + "type": "costume", + "tags": [ + "city", + "vehicle", + "bike", + "motorcycle" + ], + "info": [ + 52, + 44, + 1 + ] + }, + { + "name": "Motorcycle-c", + "md5": "6af27b2b5e1eeae9e60702630e6909a8.svg", + "type": "costume", + "tags": [ + "city", + "vehicle", + "bike", + "motorcycle" + ], + "info": [ + 52, + 44, + 1 + ] + }, + { + "name": "Motorcycle-d", + "md5": "c5d78ac1de3a7f786a4ca6dc0fe087d4.svg", + "type": "costume", + "tags": [ + "city", + "vehicle", + "bike", + "motorcycle" + ], + "info": [ + 52, + 44, + 1 + ] + }, { "name": "Mouse1-a", "md5": "e1f0c26afecbe9d4b9923d8e6bf489a8.svg", @@ -7155,7 +7483,7 @@ }, { "name": "Outfielder-a", - "md5": "fab7218666ba49eae992664acab53159.svg", + "md5": "d2e2e4a9249c4cbe479e431a4c985286.svg", "type": "costume", "tags": [ "baseball", @@ -7164,14 +7492,14 @@ "alex eben meyer" ], "info": [ - 38, - 61, + 42, + 78, 1 ] }, { "name": "Outfielder-b", - "md5": "a86813067f26947adb68897b870b1b7e.svg", + "md5": "5a960b109ae5dbe3c3ea5555894e8c13.svg", "type": "costume", "tags": [ "baseball", @@ -7180,14 +7508,14 @@ "alex eben meyer" ], "info": [ - 38, - 57, + 42, + 74, 1 ] }, { "name": "Outfielder-c", - "md5": "f6f1d2b7c6b50cbdac8cdbb55c872114.svg", + "md5": "480ab4ce1ab5586e829e1cf804824c8b.svg", "type": "costume", "tags": [ "baseball", @@ -7196,14 +7524,14 @@ "alex eben meyer" ], "info": [ - 46, - 80, + 67, + 97, 1 ] }, { "name": "Outfielder-d", - "md5": "c6fb3a8079bce9630ab9499926c16de9.svg", + "md5": "e5803b5601e0aafa8d0b227f1b1ae742.svg", "type": "costume", "tags": [ "baseball", @@ -7212,14 +7540,14 @@ "alex eben meyer" ], "info": [ - 79, - 96, + 130, + 114, 1 ] }, { "name": "Owl-a", - "md5": "a312273b198fcacf68976e3cc74fadb4.svg", + "md5": "5348f1ea36a645322e88796545ca8ca1.svg", "type": "costume", "tags": [ "animals", @@ -7227,14 +7555,14 @@ "robert hunter" ], "info": [ - 113, - 54, + 24, + 40, 1 ] }, { "name": "Owl-b", - "md5": "c9916dcfe67302367b05be7f3e5c5ddf.svg", + "md5": "3d8ba4198a03e484ed63bde584fd5bc1.svg", "type": "costume", "tags": [ "animals", @@ -7242,14 +7570,14 @@ "robert hunter" ], "info": [ - 113, - 54, + 44, + 46, 1 ] }, { "name": "Owl-c", - "md5": "8ec3a2507f1d6dc9b39f7ae5a1ebfdd3.svg", + "md5": "4e81445e70169f074ad50473a78f3381.svg", "type": "costume", "tags": [ "animals", @@ -7257,8 +7585,8 @@ "robert hunter" ], "info": [ - 113, - 54, + 109, + 41, 1 ] }, @@ -7388,16 +7716,44 @@ ] }, { - "name": "Partyhat1", - "md5": "70a7f535d8857cf9175492415361c361.svg", + "name": "Party Hat-a", + "md5": "f3ee428806f9fa6d396e30a8d6d0e741.svg", "type": "costume", "tags": [ "fashion", - "holiday" + "winter" ], "info": [ - 75, - 75, + 24, + 55, + 1 + ] + }, + { + "name": "Party Hat-b", + "md5": "71b51d9680be4aa946eb6642bc044791.svg", + "type": "costume", + "tags": [ + "fashion", + "winter" + ], + "info": [ + 40, + 61, + 1 + ] + }, + { + "name": "Party Hat-e", + "md5": "e69516f2a9bb6d632fdaba4fadd8f8f1.svg", + "type": "costume", + "tags": [ + "fashion", + "winter" + ], + "info": [ + 52, + 44, 1 ] }, @@ -7431,9 +7787,10 @@ }, { "name": "Penguin-a", - "md5": "6d85d016129c34c90ef269933a72a7a7.svg", + "md5": "0e78708b8988802d2209a34b50292bd3.svg", "type": "costume", "tags": [ + "penguin", "animals", "bird", "cold", @@ -7452,10 +7809,11 @@ }, { "name": "Penguin-b", - "md5": "e93ef702208fad2776a34f57c1c746b1.svg", + "md5": "43af2a0b7f922ec5dc94217b2f3e08e4.svg", "type": "costume", "tags": [ "animals", + "penguin", "bird", "cold", "north pole", @@ -7466,17 +7824,18 @@ "robert hunter" ], "info": [ - 36, + 26, 46, 1 ] }, { "name": "Penguin-c", - "md5": "c6d6ec90b897855831a73b3b3b8f202a.svg", + "md5": "4f35c3940957e3ffc2e83a2565942171.svg", "type": "costume", "tags": [ "animals", + "penguin", "bird", "cold", "north pole", @@ -7487,7 +7846,7 @@ "robert hunter" ], "info": [ - 36, + 35, 46, 1 ] @@ -7510,6 +7869,22 @@ }, { "name": "Penguin2-b", + "md5": "b224a582395e0847c2ef4eefcfbc4546.svg", + "type": "costume", + "tags": [ + "animals", + "bird", + "winter", + "antarctica" + ], + "info": [ + 54, + 61, + 1 + ] + }, + { + "name": "Penguin2-c", "md5": "35fec7aa5f60cca945fe0615413f1f08.svg", "type": "costume", "tags": [ @@ -7525,7 +7900,7 @@ ] }, { - "name": "Penguin2-c", + "name": "Penguin2-d", "md5": "18fa51a64ebd5518f0c5c465525346e5.svg", "type": "costume", "tags": [ @@ -7657,7 +8032,7 @@ }, { "name": "Pitcher-a", - "md5": "a8694f8f7867d95c62121e60cb8b4812.svg", + "md5": "24008f02de4b0d999d4526d34dec8b76.svg", "type": "costume", "tags": [ "baseball", @@ -7667,13 +8042,13 @@ ], "info": [ 42, - 63, + 74, 1 ] }, { "name": "Pitcher-b", - "md5": "13f363056c706870ede82798e6a8bcf6.svg", + "md5": "4644ab73c577ed878e3ccaa3d89ccc2b.svg", "type": "costume", "tags": [ "baseball", @@ -7682,14 +8057,14 @@ "alex eben meyer" ], "info": [ - 70, - 86, + 75, + 97, 1 ] }, { "name": "Pitcher-c", - "md5": "6e26bd644ecd8d1f1606261d1b8f6a75.svg", + "md5": "f46aa0b114a9977c52eeefa252953e2b.svg", "type": "costume", "tags": [ "baseball", @@ -7698,14 +8073,14 @@ "alex eben meyer" ], "info": [ - 62, - 87, + 69, + 98, 1 ] }, { "name": "Pitcher-d", - "md5": "120d823290171ad83b6b328a5ffaf60e.svg", + "md5": "d75a06e24f717e8647db27e94618e693.svg", "type": "costume", "tags": [ "baseball", @@ -7714,8 +8089,8 @@ "alex eben meyer" ], "info": [ - 82, - 46, + 84, + 57, 1 ] }, @@ -7734,9 +8109,10 @@ }, { "name": "Polar Bear-a", - "md5": "c96435a1b9ad2ed37b2c9c5aac6d1fb4.svg", + "md5": "b1d892d9f8a897bc6a446e2ff766ba74.svg", "type": "costume", "tags": [ + "bear", "animals", "cold", "north pole", @@ -7748,15 +8124,16 @@ ], "info": [ 104, - 46, + 42, 1 ] }, { "name": "Polar Bear-b", - "md5": "4f9b9061414deedeeebd369a496aba07.svg", + "md5": "54728e795213e67ac7cecca1c32fe293.svg", "type": "costume", "tags": [ + "bear", "animals", "cold", "north pole", @@ -7767,16 +8144,17 @@ "robert hunter" ], "info": [ - 104, - 46, + 87, + 47, 1 ] }, { "name": "Polar Bear-c", - "md5": "defad238b915f766449e9dcb719bb6e0.svg", + "md5": "7d5812e0aa1cde33771f9270ff521cfe.svg", "type": "costume", "tags": [ + "bear", "animals", "cold", "north pole", @@ -7788,7 +8166,7 @@ ], "info": [ 104, - 46, + 43, 1 ] }, @@ -8001,32 +8379,6 @@ 1 ] }, - { - "name": "Pumpkin-costume", - "md5": "6f7a5a88e6201082913a099438d626ef.svg", - "type": "costume", - "tags": [ - "fashion" - ], - "info": [ - 39, - 63, - 1 - ] - }, - { - "name": "Pumpkin-hat", - "md5": "22bdeaf0d8421ffb70775ed77070bee7.svg", - "type": "costume", - "tags": [ - "fashion" - ], - "info": [ - 28, - 24, - 1 - ] - }, { "name": "Puppy Back", "md5": "05630bfa94501a3e5d61ce443a0cea70.png", @@ -8177,6 +8529,36 @@ 1 ] }, + { + "name": "Radio-a", + "md5": "d2266f31369a97fdf7ec37090bf0a968.svg", + "type": "costume", + "tags": [ + "radio", + "music", + "beatbox" + ], + "info": [ + 52, + 38, + 1 + ] + }, + { + "name": "Radio-b", + "md5": "cd4cff5586d0167fd992772360bddf68.svg", + "type": "costume", + "tags": [ + "radio", + "music", + "beatbox" + ], + "info": [ + 51, + 84, + 1 + ] + }, { "name": "Rainbow", "md5": "680a806bd87a28c8b25b5f9b6347f022.svg", @@ -8195,7 +8577,7 @@ }, { "name": "Referee-a", - "md5": "0959403aeb134ed2932a85c77e261122.svg", + "md5": "7caaa6c83b0f947268866b48fbf34ed9.svg", "type": "costume", "tags": [ "sports", @@ -8204,14 +8586,14 @@ "alex eben meyer" ], "info": [ - 76, - 68, + 44, + 63, 1 ] }, { "name": "Referee-b", - "md5": "866b9e1ad2e0584216dd45fe7d50d6f5.svg", + "md5": "b26bc52a8400fa3d51201e9ab4a4dbb2.svg", "type": "costume", "tags": [ "sports", @@ -8220,14 +8602,14 @@ "alex eben meyer" ], "info": [ - 76, - 68, + 44, + 63, 1 ] }, { "name": "Referee-c", - "md5": "21a3869435fbd470ef60960a78b06b16.svg", + "md5": "f96a3af7f21c0b195b8f7a84146b6704.svg", "type": "costume", "tags": [ "sports", @@ -8236,14 +8618,14 @@ "alex eben meyer" ], "info": [ - 76, - 68, + 55, + 62, 1 ] }, { "name": "Referee-d", - "md5": "5e037aca5446a7e57093e45fe6f18c9e.svg", + "md5": "d6795f2992d899611ee5889811801e42.svg", "type": "costume", "tags": [ "sports", @@ -8252,8 +8634,8 @@ "alex eben meyer" ], "info": [ - 76, - 68, + 50, + 63, 1 ] }, @@ -8272,6 +8654,45 @@ 1 ] }, + { + "name": "Retro Robot A", + "md5": "a17c1ce38c261395ae268f3b8a9037db.svg", + "type": "costume", + "tags": [ + "robot" + ], + "info": [ + 55, + 86, + 1 + ] + }, + { + "name": "Retro Robot B", + "md5": "bfb5afe62358ef542f118299bb53d4b7.svg", + "type": "costume", + "tags": [ + "robot" + ], + "info": [ + 52, + 88, + 1 + ] + }, + { + "name": "Retro Robot C", + "md5": "f925e3bde178001133a11fa97847a9ae.svg", + "type": "costume", + "tags": [ + "robot" + ], + "info": [ + 72, + 91, + 1 + ] + }, { "name": "Ripley-a", "md5": "417ec9f25ad70281564e85e67c97aa08.svg", @@ -8424,52 +8845,7 @@ }, { "name": "Rocketship-a", - "md5": "7d51af7c52e137ef137012595bac928d.svg", - "type": "costume", - "tags": [ - "space", - "spaceship", - "wren mcdonald" - ], - "info": [ - 66, - 107, - 1 - ] - }, - { - "name": "Rocketship-b", - "md5": "ae2634705b7a4fd31f4c1d1bb0e12545.svg", - "type": "costume", - "tags": [ - "space", - "spaceship", - "wren mcdonald" - ], - "info": [ - 53, - 106, - 1 - ] - }, - { - "name": "Rocketship-c", - "md5": "3d375cd033f1a7161cae56b114f4cfdb.svg", - "type": "costume", - "tags": [ - "space", - "spaceship", - "wren mcdonald" - ], - "info": [ - 61, - 107, - 1 - ] - }, - { - "name": "Rocketship-d", - "md5": "a456964f32cd7e7bd83fe076bf29558b.svg", + "md5": "85190bb07708855c0896224340e159ed.svg", "type": "costume", "tags": [ "space", @@ -8478,13 +8854,58 @@ ], "info": [ 63, - 107, + 92, + 1 + ] + }, + { + "name": "Rocketship-b", + "md5": "2c0f804073ec656a86bbc3ebdf18bf5d.svg", + "type": "costume", + "tags": [ + "space", + "spaceship", + "wren mcdonald" + ], + "info": [ + 54, + 92, + 1 + ] + }, + { + "name": "Rocketship-c", + "md5": "13a0accfa405979e4deee12e6eb9c41e.svg", + "type": "costume", + "tags": [ + "space", + "spaceship", + "wren mcdonald" + ], + "info": [ + 59, + 91, + 1 + ] + }, + { + "name": "Rocketship-d", + "md5": "f4d0c6da1c167abbd8e86ff720ae24f9.svg", + "type": "costume", + "tags": [ + "space", + "spaceship", + "wren mcdonald" + ], + "info": [ + 65, + 91, 1 ] }, { "name": "Rocketship-e", - "md5": "5b5cc9904ba63ca2801b61a73d4dcb7e.svg", + "md5": "6e58c447e994a38b51eeb105dff5711c.svg", "type": "costume", "tags": [ "space", @@ -8493,7 +8914,7 @@ ], "info": [ 71, - 107, + 111, 1 ] }, @@ -8513,7 +8934,7 @@ }, { "name": "Rooster-a", - "md5": "8a6f3145a24db10d5a2f90975ce8140c.svg", + "md5": "db49a3dc8db15ea2b92d7934488f996b.svg", "type": "costume", "tags": [ "animals", @@ -8522,14 +8943,14 @@ "owen davey" ], "info": [ - 55, - 58, + 59, + 70, 1 ] }, { "name": "Rooster-b", - "md5": "6a14a205b117fdb0b7a6fad91a989e96.svg", + "md5": "c5aa68995a2ac203d6fe2e4dd9d86911.svg", "type": "costume", "tags": [ "animals", @@ -8538,14 +8959,14 @@ "owen davey" ], "info": [ - 55, - 59, + 58, + 70, 1 ] }, { "name": "Rooster-c", - "md5": "f23408efb8f47022da51c8fba6003cbb.svg", + "md5": "b7a5c9f9d387cc7d36d6ed935d2a46b1.svg", "type": "costume", "tags": [ "animals", @@ -8554,8 +8975,8 @@ "owen davey" ], "info": [ - 55, - 59, + 54, + 72, 1 ] }, @@ -8641,11 +9062,29 @@ ] }, { - "name": "Scarf1", + "name": "Scarf-a", + "md5": "5d671a4eb5c42e365d3c0ba1b885d9d8.svg", + "type": "costume", + "tags": [ + "fashion", + "clothing", + "blue", + "scarf" + ], + "info": [ + 54, + 23, + 1 + ] + }, + { + "name": "Scarf-b", "md5": "0c03668b4e26eba969ad803a4e810c78.svg", "type": "costume", "tags": [ "fashion", + "clothing", + "scarf", "red" ], "info": [ @@ -8655,16 +9094,17 @@ ] }, { - "name": "Scarf2", + "name": "Scarf-c", "md5": "ce66662165e2756070f1b12e0a7cb5db.svg", "type": "costume", "tags": [ - "fashion", + "fashionclothing", + "scarf", "purple" ], "info": [ - 23, - 16, + 30, + 44, 1 ] }, @@ -8757,54 +9197,6 @@ 1 ] }, - { - "name": "Shirt Blouse", - "md5": "918a507af6bbae9e7f36f0d949900838.svg", - "type": "costume", - "tags": [ - "fashion" - ], - "info": [ - 35, - 28, - 1 - ] - }, - { - "name": "Shirt Collar-a", - "md5": "b7ecf6503e27e9ab5c086eaf07d22b94.svg", - "type": "costume", - "tags": [ - "fashion" - ], - "info": [ - 30, - 57, - 1 - ] - }, - { - "name": "Shirt Collar-b", - "md5": "f1b20c3350e8a7e92a2fb1925ad4158b.svg", - "type": "costume", - "tags": [], - "info": [ - 30, - 57, - 1 - ] - }, - { - "name": "Shirt Collar-c", - "md5": "5f04b99416a794e04d0855f446780f18.svg", - "type": "costume", - "tags": [], - "info": [ - 30, - 57, - 1 - ] - }, { "name": "Shirt-a", "md5": "659465944053fe6fb6aa1ed0e11be9aa.svg", @@ -8819,45 +9211,6 @@ 1 ] }, - { - "name": "Shirt-t", - "md5": "5d7fa4f1788f03d2962f1624d6eac800.svg", - "type": "costume", - "tags": [ - "fashion" - ], - "info": [ - 39, - 28, - 1 - ] - }, - { - "name": "Shirt2-a", - "md5": "5946e7a1e36c6d97ae47d41dd8595a41.svg", - "type": "costume", - "tags": [ - "fashion" - ], - "info": [ - 39, - 48, - 1 - ] - }, - { - "name": "Shirt2-a2", - "md5": "5b78ab09592126b6bbe2d4907d203909.svg", - "type": "costume", - "tags": [ - "fashion" - ], - "info": [ - 39, - 48, - 1 - ] - }, { "name": "Shoes-a", "md5": "4639a1af5bc91f1a6f14e822cd46972f.svg", @@ -8914,32 +9267,6 @@ 1 ] }, - { - "name": "Shoes1", - "md5": "ffab4cc284070b50ac317e515f59f7d8.svg", - "type": "costume", - "tags": [ - "fashion" - ], - "info": [ - 36, - 23, - 1 - ] - }, - { - "name": "Shoes2", - "md5": "1dc5d568d98405c370b91a230397a7f9.svg", - "type": "costume", - "tags": [ - "fashion" - ], - "info": [ - 40, - 8, - 1 - ] - }, { "name": "Shorts-a", "md5": "d9b580c913c0d1d1a996dd733af91e68.svg", @@ -8978,7 +9305,7 @@ "type": "costume", "tags": [ "fashion", - " pants", + "pants", "shorts", "clothing" ], @@ -9002,24 +9329,9 @@ 1 ] }, - { - "name": "Skates", - "md5": "97936eccad5c8d86ea9008c5bbb50b63.svg", - "type": "costume", - "tags": [ - "fashion", - "winter", - "sports" - ], - "info": [ - 39, - 19, - 1 - ] - }, { "name": "Skeleton-a", - "md5": "b6ddeecf18e40b99bde7e466759d5a5b.svg", + "md5": "e06f6eaf4fd27ff22f4c8207aa13c967.svg", "type": "costume", "tags": [ "fantasy", @@ -9030,14 +9342,14 @@ "alex eben meyer" ], "info": [ - 75, - 90, + 59, + 100, 1 ] }, { "name": "Skeleton-b", - "md5": "ba3f455026f5d7778af229ddddf83206.svg", + "md5": "06769ddf5fad85e101c7beda554d60b9.svg", "type": "costume", "tags": [ "fantasy", @@ -9048,32 +9360,14 @@ "alex eben meyer" ], "info": [ - 75, - 90, - 1 - ] - }, - { - "name": "Skeleton-c", - "md5": "af8fb44231f7891d52db5b4ad5c0f58b.svg", - "type": "costume", - "tags": [ - "fantasy", - "spooky", - "halloween", - "bones", - "monster", - "alex eben meyer" - ], - "info": [ - 75, + 69, 90, 1 ] }, { "name": "Skeleton-d", - "md5": "355eb1591be72ea1117b6a4e153010b1.svg", + "md5": "155efd0dfdc59a0e2a546a4a7d068ece.svg", "type": "costume", "tags": [ "fantasy", @@ -9084,14 +9378,14 @@ "alex eben meyer" ], "info": [ - 75, - 90, + 51, + 100, 1 ] }, { "name": "Skeleton-e", - "md5": "d3497858a3b5ad938e41a34bb8103c77.svg", + "md5": "c41a11468602522cdc6dec7043215f65.svg", "type": "costume", "tags": [ "fantasy", @@ -9102,8 +9396,8 @@ "alex eben meyer" ], "info": [ - 75, - 90, + 55, + 89, 1 ] }, @@ -9182,7 +9476,7 @@ }, { "name": "Soccer Ball", - "md5": "81ff5ad24454e7a0f1f3ae863bb4dd25.svg", + "md5": "73eaf05fed60e9d65867508736f84569.svg", "type": "costume", "tags": [ "sports", @@ -9191,8 +9485,8 @@ "alex eben meyer" ], "info": [ - 26, - 26, + 23, + 22, 1 ] }, @@ -9291,7 +9585,7 @@ }, { "name": "Strawberry-a", - "md5": "734556fb8e14740f2cbc971238b71d47.svg", + "md5": "b4cff4b414b5f7fbf25c7c45abfb6c62.svg", "type": "costume", "tags": [ "food", @@ -9299,14 +9593,14 @@ "alex eben meyer" ], "info": [ - 57, - 58, + 31, + 47, 1 ] }, { "name": "Strawberry-b", - "md5": "77dadaa80bc5156f655c2196f57972f7.svg", + "md5": "ebb8720dc679772e3dcb1b49e0503aa4.svg", "type": "costume", "tags": [ "food", @@ -9314,14 +9608,14 @@ "alex eben meyer" ], "info": [ - 57, - 58, + 43, + 47, 1 ] }, { "name": "Strawberry-c", - "md5": "19f933b3cf3e8e150753f8fb9bb7a779.svg", + "md5": "278dc229a71e2fbff475908ca5c2483e.svg", "type": "costume", "tags": [ "food", @@ -9329,14 +9623,14 @@ "alex eben meyer" ], "info": [ - 57, - 58, + 43, + 47, 1 ] }, { "name": "Strawberry-d", - "md5": "8e8057da8457e6167de36b7d3d28b4bb.svg", + "md5": "68a023e80cb9eff2350374361c034f08.svg", "type": "costume", "tags": [ "food", @@ -9344,14 +9638,14 @@ "alex eben meyer" ], "info": [ - 57, - 58, + 41, + 47, 1 ] }, { "name": "Strawberry-e", - "md5": "5eb63e64b83f5aa5b75a55329a34efec.svg", + "md5": "9fef0cd91ba59163d74f4464b02824b8.svg", "type": "costume", "tags": [ "food", @@ -9359,8 +9653,8 @@ "alex eben meyer" ], "info": [ - 57, - 58, + 32, + 36, 1 ] }, @@ -9383,7 +9677,7 @@ ] }, { - "name": "Sunglasses1", + "name": "Sunglasses-a", "md5": "424393e8705aeadcfecb8559ce4dcea2.svg", "type": "costume", "tags": [ @@ -9397,7 +9691,7 @@ ] }, { - "name": "Sunglasses2", + "name": "Sunglasses-b", "md5": "3185d2295bbf2c5ebd0688c9e4f13076.svg", "type": "costume", "tags": [ @@ -9410,6 +9704,36 @@ 1 ] }, + { + "name": "Tabla-a", + "md5": "68ce53b53fcc68744584c28d20144c4f.svg", + "type": "costume", + "tags": [ + "drums", + " music", + " ericr" + ], + "info": [ + 79, + 67, + 1 + ] + }, + { + "name": "Tabla-b", + "md5": "01b6ffb8691d32be10fabc77ddfb55b0.svg", + "type": "costume", + "tags": [ + "drums", + " music", + " ericr" + ], + "info": [ + 86, + 73, + 1 + ] + }, { "name": "Taco", "md5": "bc78fb90ed373d56c11d5fafa4203ccd.svg", @@ -9443,71 +9767,71 @@ }, { "name": "Takeout-a", - "md5": "0cfdefe0df1a032b90c8facd9f5dbe1f.svg", + "md5": "05c010b2425c4433353ff85d43ed7a1d.svg", "type": "costume", "tags": [ "food", "alex eben meyer" ], "info": [ - 78, - 61, + 33, + 41, 1 ] }, { "name": "Takeout-b", - "md5": "48b19c48e32c98a35836ee40e3a7accf.svg", + "md5": "26c3be261cbb0327eaa1afe0467c80d0.svg", "type": "costume", "tags": [ "food", "alex eben meyer" ], "info": [ - 78, - 61, + 33, + 42, 1 ] }, { "name": "Takeout-c", - "md5": "22d33d87883f8fb26c642eccc9b339f0.svg", + "md5": "801fa349b34531a15d467fef2b940f87.svg", "type": "costume", "tags": [ "food", "alex eben meyer" ], "info": [ - 78, - 61, + 33, + 53, 1 ] }, { "name": "Takeout-d", - "md5": "262f1a3730d669dc9d43b3853e397361.svg", + "md5": "24e21e2846b59cdf2c390ebe985b3c74.svg", "type": "costume", "tags": [ "food", "alex eben meyer" ], "info": [ - 78, - 61, + 40, + 42, 1 ] }, { "name": "Takeout-e", - "md5": "528e9df8c3bd173867be4143f8563e87.svg", + "md5": "95584a98e22df5ca7370f5c01ebe6d06.svg", "type": "costume", "tags": [ "food", "alex eben meyer" ], "info": [ - 78, - 61, + 41, + 35, 1 ] }, @@ -9611,7 +9935,7 @@ }, { "name": "Ten80 Stance", - "md5": "90ca0205c56c6c7f0e8892469de49bda.png", + "md5": "f60f99278455c843b7833fb7615428dd.png", "type": "costume", "tags": [ "people", @@ -9869,37 +10193,85 @@ 1 ] }, + { + "name": "Truck-a", + "md5": "51404247db5cdbc082336dfa4c03d675.svg", + "type": "costume", + "tags": [ + "truck", + "city", + "car", + "vehicle" + ], + "info": [ + 174, + 49, + 1 + ] + }, + { + "name": "Truck-b", + "md5": "1887b8233d1b0728f715a3985f0e01b6.svg", + "type": "costume", + "tags": [ + "truck", + "city", + "car", + "vehicle" + ], + "info": [ + 174, + 58, + 1 + ] + }, + { + "name": "Truck-c", + "md5": "e45bc3f87d9f603e588abe14b63b8e90.svg", + "type": "costume", + "tags": [ + "truck", + "city", + "car", + "vehicle" + ], + "info": [ + 174, + 58, + 1 + ] + }, { "name": "Trumpet-a", - "md5": "8fa7459ed5877bb14c6625e688be70e7.svg", + "md5": "36a48cc311fd28b4517e15d44b11587c.svg", "type": "costume", "tags": [ "music", "andrew rae" ], "info": [ - 37, - 73, + 57, + 38, 1 ] }, { "name": "Trumpet-b", - "md5": "7cedda5ec925118f237094cd05381e5d.svg", + "md5": "c04cdfd5f05ff2817b6ab78027ef5d22.svg", "type": "costume", "tags": [ "music", "andrew rae" ], "info": [ + 55, 37, - 73, 1 ] }, { "name": "Unicorn", - "md5": "bd2cf980966c13d5ad3ab403bae4bb05.svg", + "md5": "c491fd1867375aa0160b013788d188e5.svg", "type": "costume", "tags": [ "fantasy", @@ -9907,8 +10279,8 @@ "ipzy" ], "info": [ - 100, - 100, + 91, + 95, 1 ] }, @@ -9918,6 +10290,7 @@ "type": "costume", "tags": [ "fantasy", + "animals", "horse", "horn", "rainbow" @@ -9929,8 +10302,8 @@ ] }, { - "name": "Unicorn-run-a", - "md5": "c6456df027561c74b70b8b25ab83fec9.svg", + "name": "Unicorn Running-a", + "md5": "d938a2bfbac8f3caf713d189fd993af5.svg", "type": "costume", "tags": [ "fantasy", @@ -9939,14 +10312,14 @@ "walking" ], "info": [ - 130, - 100, + 118, + 90, 1 ] }, { - "name": "Unicorn-run-b", - "md5": "f2d6c5b62b56d0cc51598126c74e0ace.svg", + "name": "Unicorn Running-b", + "md5": "663618fcc0484879de6ad485b288c444.svg", "type": "costume", "tags": [ "fantasy", @@ -9955,14 +10328,14 @@ "walking" ], "info": [ - 130, - 100, + 120, + 89, 1 ] }, { - "name": "Unicorn-run-c", - "md5": "fba367b11a710bf542333649c4d68e1e.svg", + "name": "Unicorn Running-c", + "md5": "ceb2f88fe182bc61af4e3864f8b46bba.svg", "type": "costume", "tags": [ "fantasy", @@ -9971,14 +10344,14 @@ "walking" ], "info": [ - 130, - 100, + 121, + 90, 1 ] }, { - "name": "Unicorn-run-d", - "md5": "20bad70a67e0c7ed3255c65780cdb2a8.svg", + "name": "Unicorn Running-d", + "md5": "dddad08a2d03f406307ec7155f43c51c.svg", "type": "costume", "tags": [ "fantasy", @@ -9987,14 +10360,14 @@ "walking" ], "info": [ - 130, - 100, + 120, + 87, 1 ] }, { - "name": "Unicorn-run-e", - "md5": "39db08aafdde1542e755b8ead8df7a1f.svg", + "name": "Unicorn Running-e", + "md5": "cf3a00e72f3fc20d9ad15379fdb7b424.svg", "type": "costume", "tags": [ "fantasy", @@ -10003,14 +10376,14 @@ "walking" ], "info": [ - 130, - 100, + 119, + 90, 1 ] }, { - "name": "Unicorn-run-f", - "md5": "d66d0625702433a4cbd7591e39676700.svg", + "name": "Unicorn Running-f", + "md5": "264a70e872af2d3f6b2abad85daf82ae.svg", "type": "costume", "tags": [ "fantasy", @@ -10019,34 +10392,8 @@ "walking" ], "info": [ - 130, - 100, - 1 - ] - }, - { - "name": "Vest-a", - "md5": "f8285ca9564451c62a0e3d75b8a714e8.svg", - "type": "costume", - "tags": [ - "fashon" - ], - "info": [ - 18, - 62, - 1 - ] - }, - { - "name": "Vest-b", - "md5": "77d553eea3910ab8f5bac3d2da601061.svg", - "type": "costume", - "tags": [ - "fashon" - ], - "info": [ - 18, - 62, + 117, + 86, 1 ] }, @@ -10130,6 +10477,21 @@ 1 ] }, + { + "name": "Winter Hat", + "md5": "62678324450bac2154703e2978e8ab61.svg", + "type": "costume", + "tags": [ + "fashion", + "winter", + "hat" + ], + "info": [ + 35, + 39, + 1 + ] + }, { "name": "Witch", "md5": "c991196a708294535a1dbdce7189c23c.svg", @@ -10220,6 +10582,21 @@ 1 ] }, + { + "name": "Wizard Hat", + "md5": "2da561a7e5cbfcda613f4750020d4aa5.svg", + "type": "costume", + "tags": [ + "fashion", + "fantasy", + "winter" + ], + "info": [ + 34, + 60, + 1 + ] + }, { "name": "Wizard-a", "md5": "9632aab80fce1c5bdb58150b29cb0067.svg", @@ -10281,8 +10658,10 @@ "tags": [ "fantasy", "people", + "animals", "ipzy", "castle", + "animals", "amphibians", "magic" ], @@ -10299,6 +10678,7 @@ "tags": [ "fantasy", "people", + "animals", "ipzy", "castle", "amphibians", @@ -10312,11 +10692,12 @@ }, { "name": "Zebra-a", - "md5": "d8392aea6e767a4b5032cffd04e688e3.svg", + "md5": "f58632e6b34fa8f9b35219e52ed2c864.svg", "type": "costume", "tags": [ "animals", "savanna", + "zebra", "robert hunter" ], "info": [ @@ -10327,30 +10708,16 @@ }, { "name": "Zebra-b", - "md5": "3a6be1df1d9d9865b6bba4f569de6793.svg", + "md5": "ea23797598a55938a2d46f2b0a389fd6.svg", "type": "costume", "tags": [ "animals", "savanna", + "zebra", "robert hunter" ], "info": [ - 97, - 56, - 1 - ] - }, - { - "name": "Zebra-c", - "md5": "b0d2d3e746493640fc34fd7b2efd6612.svg", - "type": "costume", - "tags": [ - "animals", - "savanna", - "robert hunter" - ], - "info": [ - 97, + 96, 56, 1 ] @@ -11229,7 +11596,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 23, @@ -11243,7 +11610,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 23, @@ -11257,7 +11624,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 22, @@ -11271,7 +11638,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 20, @@ -11285,7 +11652,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 19, @@ -11299,7 +11666,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 18, @@ -11313,7 +11680,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 22, @@ -11327,7 +11694,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 22, @@ -11341,7 +11708,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 20, @@ -11355,7 +11722,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 25, @@ -11369,7 +11736,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 25, @@ -11383,7 +11750,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 22, @@ -11397,7 +11764,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 20, @@ -11411,7 +11778,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 20, @@ -11425,7 +11792,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 18, @@ -11439,7 +11806,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 18, @@ -11453,7 +11820,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 18, @@ -11467,7 +11834,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 16, @@ -11481,7 +11848,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 23, @@ -11495,7 +11862,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 23, @@ -11509,7 +11876,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 21, @@ -11523,7 +11890,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 24, @@ -11537,7 +11904,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 24, @@ -11551,7 +11918,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 22, @@ -11565,7 +11932,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 9, @@ -11579,7 +11946,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 9, @@ -11593,7 +11960,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 7, @@ -11607,7 +11974,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 14, @@ -11621,7 +11988,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 14, @@ -11635,7 +12002,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 12, @@ -11649,7 +12016,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 24, @@ -11663,7 +12030,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 24, @@ -11677,7 +12044,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 21, @@ -11691,7 +12058,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 19, @@ -11705,7 +12072,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 19, @@ -11719,7 +12086,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 17, @@ -11733,7 +12100,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 30, @@ -11747,7 +12114,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 30, @@ -11761,7 +12128,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 27, @@ -11775,7 +12142,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 26, @@ -11789,7 +12156,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 26, @@ -11803,7 +12170,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 24, @@ -11817,7 +12184,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 25, @@ -11831,7 +12198,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 24, @@ -11845,7 +12212,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 22, @@ -11859,7 +12226,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 20, @@ -11873,7 +12240,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 20, @@ -11887,7 +12254,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 17, @@ -11901,7 +12268,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 25, @@ -11915,7 +12282,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 25, @@ -11929,7 +12296,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 22, @@ -11943,7 +12310,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 22, @@ -11957,7 +12324,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 22, @@ -11971,7 +12338,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 20, @@ -11985,7 +12352,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 16, @@ -11999,7 +12366,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 16, @@ -12013,7 +12380,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 14, @@ -12027,7 +12394,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 22, @@ -12041,7 +12408,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 22, @@ -12055,7 +12422,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 20, @@ -12069,7 +12436,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 24, @@ -12083,7 +12450,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 24, @@ -12097,7 +12464,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 21, @@ -12111,7 +12478,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 25, @@ -12125,7 +12492,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 25, @@ -12139,7 +12506,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 22, @@ -12153,7 +12520,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 37, @@ -12167,7 +12534,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 37, @@ -12181,7 +12548,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 34, @@ -12195,7 +12562,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 22, @@ -12209,7 +12576,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 22, @@ -12223,7 +12590,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 20, @@ -12237,7 +12604,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 22, @@ -12251,7 +12618,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 22, @@ -12265,7 +12632,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 20, @@ -12279,7 +12646,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 19, @@ -12293,7 +12660,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 19, @@ -12307,7 +12674,7 @@ "type": "costume", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 17, diff --git a/src/lib/libraries/decks/animate-char/animate-char-add-sound.gif b/src/lib/libraries/decks/animate-char/animate-char-add-sound.gif new file mode 100755 index 000000000..21f533e7f Binary files /dev/null and b/src/lib/libraries/decks/animate-char/animate-char-add-sound.gif differ diff --git a/src/lib/libraries/decks/animate-char/animate-char-change-color.gif b/src/lib/libraries/decks/animate-char/animate-char-change-color.gif new file mode 100644 index 000000000..86e7d9188 Binary files /dev/null and b/src/lib/libraries/decks/animate-char/animate-char-change-color.gif differ diff --git a/src/lib/libraries/decks/animate-char/animate-char-jump.gif b/src/lib/libraries/decks/animate-char/animate-char-jump.gif new file mode 100755 index 000000000..7a9138eec Binary files /dev/null and b/src/lib/libraries/decks/animate-char/animate-char-jump.gif differ diff --git a/src/lib/libraries/decks/animate-char/animate-char-move.gif b/src/lib/libraries/decks/animate-char/animate-char-move.gif new file mode 100755 index 000000000..1f8a4765a Binary files /dev/null and b/src/lib/libraries/decks/animate-char/animate-char-move.gif differ diff --git a/src/lib/libraries/decks/animate-char/animate-char-pick-another-sprite.gif b/src/lib/libraries/decks/animate-char/animate-char-pick-another-sprite.gif new file mode 100755 index 000000000..97bed56b4 Binary files /dev/null and b/src/lib/libraries/decks/animate-char/animate-char-pick-another-sprite.gif differ diff --git a/src/lib/libraries/decks/animate-char/animate-char-pick-backdrop.gif b/src/lib/libraries/decks/animate-char/animate-char-pick-backdrop.gif new file mode 100755 index 000000000..ac0317323 Binary files /dev/null and b/src/lib/libraries/decks/animate-char/animate-char-pick-backdrop.gif differ diff --git a/src/lib/libraries/decks/animate-char/animate-char-pick-sprite.gif b/src/lib/libraries/decks/animate-char/animate-char-pick-sprite.gif new file mode 100755 index 000000000..e66e11451 Binary files /dev/null and b/src/lib/libraries/decks/animate-char/animate-char-pick-sprite.gif differ diff --git a/src/lib/libraries/decks/animate-char/animate-char-say-something.gif b/src/lib/libraries/decks/animate-char/animate-char-say-something.gif new file mode 100755 index 000000000..7617802a7 Binary files /dev/null and b/src/lib/libraries/decks/animate-char/animate-char-say-something.gif differ diff --git a/src/lib/libraries/decks/animate-char/animate-char-talk.gif b/src/lib/libraries/decks/animate-char/animate-char-talk.gif new file mode 100755 index 000000000..2147c46a4 Binary files /dev/null and b/src/lib/libraries/decks/animate-char/animate-char-talk.gif differ diff --git a/src/lib/libraries/decks/animate-char/lib_Animate_a_Character.jpg b/src/lib/libraries/decks/animate-char/lib_Animate_a_Character.jpg new file mode 100644 index 000000000..1fddfb5c5 Binary files /dev/null and b/src/lib/libraries/decks/animate-char/lib_Animate_a_Character.jpg differ diff --git a/src/lib/libraries/decks/index.jsx b/src/lib/libraries/decks/index.jsx index bc6c40202..78aeb6f29 100644 --- a/src/lib/libraries/decks/index.jsx +++ b/src/lib/libraries/decks/index.jsx @@ -74,6 +74,37 @@ import stepRandom from './game/game-random-position.gif'; import stepGameChangeColor from './game/game-change-color.gif'; import stepResetScore from './game/game-reset-score.gif'; +// Animate A Character +import libraryAnimateChar from './animate-char/lib_Animate_a_Character.jpg'; +import stepAnimateCharPickBackdrop from './animate-char/animate-char-pick-backdrop.gif'; +import stepAnimateCharPickSprite from './animate-char/animate-char-pick-sprite.gif'; +import stepAnimateCharSaySomething from './animate-char/animate-char-say-something.gif'; +import stepAnimateCharAddSound from './animate-char/animate-char-add-sound.gif'; +import stepAnimateCharTalk from './animate-char/animate-char-talk.gif'; +import stepAnimateCharMove from './animate-char/animate-char-move.gif'; +import stepAnimateCharJump from './animate-char/animate-char-jump.gif'; +import stepAnimateCharChangeColor from './animate-char/animate-char-change-color.gif'; + +// Tell A Story +import libraryStory from './story/lib-tell-a-story.jpg'; +import stepStoryPickBackdrop from './story/story-pick-backdrop.gif'; +import stepStoryPickSprite from './story/story-pick-sprite.gif'; +import stepStoryCharSaySomething from './story/story-say-something.gif'; +import stepStoryCharAddAnotherSprite from './story/story-pick-another-sprite.gif'; +import stepStoryFlip from './story/story-flip.gif'; +import stepStoryConversation from './story/story-conversation.gif'; +import stepStoryAddAnotherBackdrop from './story/story-add-background-2.gif'; +import stepStorySwitchBackdrop from './story/story-switch-backdrop.gif'; +import stepStoryHide from './story/story-hide-character.gif'; +import stepStoryShow from './story/story-show-character.gif'; + +// Video Sensing +import libraryVideoSens from './videosensing/lib_video_sensing.jpg'; +import stepVideoSensAddExtension from './videosensing/videosens-add-extension.gif'; +import stepVideoSensPet from './videosensing/videosens-pet-cat.gif'; +import stepVideoSensAnimate from './videosensing/videosens-animate.gif'; +import stepVideoSensPop from './videosensing/videosens-pop.gif'; + // Videos import recordASound from './videos/record-a-sound.jpg'; import glideAroundThumb from './videos/glide-around.jpg'; @@ -675,6 +706,268 @@ export default { ], urlId: 'chase-game' }, + 'Animate-A-Character': { + name: ( + + ), + img: libraryAnimateChar, + steps: [{ + video: 's228u3g5u9' + }, + { + title: ( + + ), + image: stepAnimateCharPickBackdrop + }, { + title: ( + + ), + image: stepAnimateCharPickSprite + }, { + title: ( + + ), + image: stepAnimateCharSaySomething + }, { + title: ( + + ), + image: stepAnimateCharAddSound + }, { + title: ( + + ), + image: stepAnimateCharTalk + }, { + title: ( + + ), + image: stepAnimateCharMove + }, { + title: ( + + ), + image: stepAnimateCharJump + }, { + title: ( + + ), + image: stepAnimateCharChangeColor + }, { + deckIds: [ + 'Chase-Game', + 'Tell-A-Story' + ] + } + ], + urlId: 'animate-a-character' + }, + 'Tell-A-Story': { + name: ( + + ), + img: libraryStory, + steps: [{ + video: 'stah7jjorp' + }, + { + title: ( + + ), + image: stepStoryPickBackdrop + }, { + title: ( + + ), + image: stepStoryPickSprite + }, { + title: ( + + ), + image: stepStoryCharSaySomething + }, { + title: ( + + ), + image: stepStoryCharAddAnotherSprite + }, { + title: ( + + ), + image: stepStoryFlip + }, { + title: ( + + ), + image: stepStoryConversation + }, { + title: ( + + ), + image: stepStoryAddAnotherBackdrop + }, { + title: ( + + ), + image: stepStorySwitchBackdrop + }, { + title: ( + + ), + image: stepStoryHide + }, { + title: ( + + ), + image: stepStoryShow + }, + { + deckIds: [ + 'say-it-out-loud', + 'record-a-sound' + ] + } + ], + urlId: 'tell-a-story' + }, + 'Video-Sensing': { + name: ( + + ), + img: libraryVideoSens, + steps: [{ + video: '3pd1z110d6' + }, + { + title: ( + + ), + image: stepVideoSensAddExtension + }, { + title: ( + + ), + image: stepVideoSensPet + }, { + title: ( + + ), + image: stepVideoSensAnimate + }, { + title: ( + + ), + image: stepVideoSensPop + }, { + deckIds: [ + 'Make-Music', + 'add-effects' + ] + } + ], + urlId: 'video-sensing' + }, 'add-sprite': { name: ( ), extensionId: 'text2speech', + collaborator: 'Amazon Web Services', iconURL: text2speechImage, + insetIconURL: text2speechInsetImage, description: ( ), - featured: true + featured: true, + internetConnectionRequired: true }, { name: ( @@ -104,7 +117,9 @@ export default [ /> ), extensionId: 'translate', + collaborator: 'Google', iconURL: translateImage, + insetIconURL: translateInsetImage, description: ( ), + featured: true, + internetConnectionRequired: true + }, + { + name: 'Makey Makey', + extensionId: 'makeymakey', + collaborator: 'JoyLabz', + iconURL: makeymakeyImage, + insetIconURL: makeymakeyInsetImage, + description: ( + + ), featured: true }, { name: 'micro:bit', extensionId: 'microbit', + collaborator: 'micro:bit', iconURL: microbitImage, + insetIconURL: microbitMenuImage, description: ( \ No newline at end of file diff --git a/src/lib/libraries/extensions/makeymakey.png b/src/lib/libraries/extensions/makeymakey.png new file mode 100644 index 000000000..8acd35bc1 Binary files /dev/null and b/src/lib/libraries/extensions/makeymakey.png differ diff --git a/src/lib/libraries/extensions/music-small.svg b/src/lib/libraries/extensions/music-small.svg new file mode 100644 index 000000000..7ffde85e8 --- /dev/null +++ b/src/lib/libraries/extensions/music-small.svg @@ -0,0 +1 @@ +music-block-icon \ No newline at end of file diff --git a/src/lib/libraries/extensions/pen-small.svg b/src/lib/libraries/extensions/pen-small.svg new file mode 100644 index 000000000..452451e2a --- /dev/null +++ b/src/lib/libraries/extensions/pen-small.svg @@ -0,0 +1 @@ +pen-icon \ No newline at end of file diff --git a/src/lib/libraries/extensions/text2speech-small.svg b/src/lib/libraries/extensions/text2speech-small.svg new file mode 100644 index 000000000..1ce14e3fc --- /dev/null +++ b/src/lib/libraries/extensions/text2speech-small.svg @@ -0,0 +1,12 @@ + + + + Extensions/Software/Text-to-Speech-Block + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/src/lib/libraries/extensions/translate-small.png b/src/lib/libraries/extensions/translate-small.png new file mode 100644 index 000000000..f18565023 Binary files /dev/null and b/src/lib/libraries/extensions/translate-small.png differ diff --git a/src/lib/libraries/extensions/video-sensing-small.svg b/src/lib/libraries/extensions/video-sensing-small.svg new file mode 100644 index 000000000..6837da1af --- /dev/null +++ b/src/lib/libraries/extensions/video-sensing-small.svg @@ -0,0 +1,15 @@ + + + + Extensions/Software/Video-Sensing-Block + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/src/lib/libraries/sounds.json b/src/lib/libraries/sounds.json index 6b5eb8d4d..077b85bf8 100644 --- a/src/lib/libraries/sounds.json +++ b/src/lib/libraries/sounds.json @@ -29,7 +29,11 @@ "sampleCount": 44100, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "A Elec Piano", @@ -37,7 +41,12 @@ "sampleCount": 44100, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "note", + "piano", + "keyboard" + ] }, { "name": "A Guitar", @@ -45,7 +54,11 @@ "sampleCount": 31872, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "A Minor Ukulele", @@ -78,7 +91,11 @@ "sampleCount": 6472, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "A Trombone", @@ -177,7 +194,11 @@ "sampleCount": 44100, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "B Elec Piano", @@ -185,7 +206,12 @@ "sampleCount": 44100, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "note", + "piano", + "keyboard" + ] }, { "name": "B Guitar", @@ -193,7 +219,11 @@ "sampleCount": 29504, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "B Piano", @@ -213,7 +243,11 @@ "sampleCount": 9555, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "B Trombone", @@ -340,9 +374,7 @@ "sampleCount": 3840, "rate": 11025, "format": "", - "tags": [ - "animals" - ] + "tags": [] }, { "name": "Birthday", @@ -440,10 +472,7 @@ "sampleCount": 45056, "rate": 11025, "format": "", - "tags": [ - "effects", - "water" - ] + "tags": [] }, { "name": "Buzz Whir", @@ -483,7 +512,11 @@ "sampleCount": 44100, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "C Elec Piano", @@ -491,7 +524,12 @@ "sampleCount": 44100, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "note", + "piano", + "keyboard" + ] }, { "name": "C Guitar", @@ -499,7 +537,11 @@ "sampleCount": 44864, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "C Major Ukulele", @@ -532,7 +574,11 @@ "sampleCount": 9491, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "C Trombone", @@ -588,7 +634,11 @@ "sampleCount": 44100, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "C2 Elec Piano", @@ -596,7 +646,12 @@ "sampleCount": 44100, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "note", + "piano", + "keyboard" + ] }, { "name": "C2 Guitar", @@ -624,7 +679,11 @@ "sampleCount": 7349, "rate": 22050, "format": "adpcm", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "C2 Trombone", @@ -1090,7 +1149,11 @@ "sampleCount": 44100, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "D Elec Piano", @@ -1098,7 +1161,12 @@ "sampleCount": 44100, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "note", + "piano", + "keyboard" + ] }, { "name": "D Guitar", @@ -1106,7 +1174,11 @@ "sampleCount": 41120, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "D Piano", @@ -1126,7 +1198,11 @@ "sampleCount": 9555, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "D Trombone", @@ -1597,7 +1673,11 @@ "sampleCount": 44100, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "E Elec Piano", @@ -1605,7 +1685,12 @@ "sampleCount": 44100, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "note", + "piano", + "keyboard" + ] }, { "name": "E Guitar", @@ -1613,7 +1698,11 @@ "sampleCount": 38400, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "E Piano", @@ -1633,7 +1722,11 @@ "sampleCount": 7489, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "E Trombone", @@ -1785,7 +1878,11 @@ "sampleCount": 44100, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "F Elec Piano", @@ -1793,7 +1890,12 @@ "sampleCount": 44100, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "note", + "piano", + "keyboard" + ] }, { "name": "F Guitar", @@ -1801,7 +1903,11 @@ "sampleCount": 36416, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "F Major Ukulele", @@ -1834,7 +1940,11 @@ "sampleCount": 7361, "rate": 22050, "format": "adpcm", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "F Trombone", @@ -1866,11 +1976,7 @@ "sampleCount": 11247, "rate": 22050, "format": "", - "tags": [ - "effects", - "fantasy", - "magic" - ] + "tags": [] }, { "name": "Finger Snap", @@ -1933,7 +2039,11 @@ "sampleCount": 44100, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "G Elec Piano", @@ -1941,7 +2051,12 @@ "sampleCount": 44100, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "note", + "piano", + "keyboard" + ] }, { "name": "G Guitar", @@ -1949,7 +2064,11 @@ "sampleCount": 33600, "rate": 22050, "format": "", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "G Piano", @@ -1969,7 +2088,11 @@ "sampleCount": 7349, "rate": 22050, "format": "adpcm", - "tags": [] + "tags": [ + "music", + "instruments", + "notes" + ] }, { "name": "G Trombone", @@ -2194,9 +2317,9 @@ "rate": 22050, "format": "", "tags": [ - "music", - "percussion", - "drums" + "drums", + "instrument", + "percussion" ] }, { @@ -2206,9 +2329,9 @@ "rate": 22050, "format": "", "tags": [ - "music", - "percussion", - "drums" + "drums", + "instrument", + "percussion" ] }, { @@ -2218,9 +2341,9 @@ "rate": 22050, "format": "", "tags": [ - "music", - "percussion", - "drums" + "drums", + "instrument", + "percussion" ] }, { @@ -2304,10 +2427,7 @@ "sampleCount": 38336, "rate": 11025, "format": "", - "tags": [ - "animals", - "effects" - ] + "tags": [] }, { "name": "Human Beatbox1", @@ -2466,9 +2586,9 @@ "rate": 22050, "format": "", "tags": [ - "music", - "percussion", - "drums" + "drums", + "instrument", + "percussion" ] }, { @@ -2478,9 +2598,9 @@ "rate": 22050, "format": "", "tags": [ - "music", - "percussion", - "drums" + "drums", + "instrument", + "percussion" ] }, { @@ -2512,9 +2632,9 @@ "rate": 22050, "format": "", "tags": [ - "music", - "percussion", - "drums" + "drums", + "instrument", + "percussion" ] }, { @@ -2667,9 +2787,9 @@ "rate": 22050, "format": "", "tags": [ - "music", - "percussion", - "drums" + "drums", + "instrument", + "percussion" ] }, { @@ -2739,10 +2859,7 @@ "sampleCount": 8111, "rate": 11025, "format": "", - "tags": [ - "animals", - "birds" - ] + "tags": [] }, { "name": "Party Noise", @@ -2821,16 +2938,6 @@ "format": "", "tags": [] }, - { - "name": "Pop2", - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "", - "tags": [ - "effects" - ] - }, { "name": "Rain", "md5": "b5db20c28ef4946137129b47772dcf69.wav", @@ -2850,10 +2957,7 @@ "sampleCount": 13184, "rate": 22050, "format": "", - "tags": [ - "effects", - "percussion" - ] + "tags": [] }, { "name": "Referee Whistle", @@ -2946,10 +3050,7 @@ "sampleCount": 17110, "rate": 11025, "format": "", - "tags": [ - "animals", - "birds" - ] + "tags": [] }, { "name": "Scrambling Feet", @@ -3452,9 +3553,9 @@ "rate": 22050, "format": "", "tags": [ - "percussion", "drums", - "music" + "instrument", + "percussion" ] }, { @@ -3828,10 +3929,7 @@ "sampleCount": 43008, "rate": 11025, "format": "", - "tags": [ - "animals", - "dramatic" - ] + "tags": [] }, { "name": "Wood Tap", diff --git a/src/lib/libraries/sprite-tags.js b/src/lib/libraries/sprite-tags.js index 8a12e1e41..22f3b0697 100644 --- a/src/lib/libraries/sprite-tags.js +++ b/src/lib/libraries/sprite-tags.js @@ -7,5 +7,6 @@ export default [ {tag: 'music', intlLabel: messages.music}, {tag: 'sports', intlLabel: messages.sports}, {tag: 'food', intlLabel: messages.food}, - {tag: 'fashion', intlLabel: messages.fashion} + {tag: 'fashion', intlLabel: messages.fashion}, + {tag: 'letters', intlLabel: messages.letters} ]; diff --git a/src/lib/libraries/sprites.json b/src/lib/libraries/sprites.json index 3b9596b7a..961ca5c99 100644 --- a/src/lib/libraries/sprites.json +++ b/src/lib/libraries/sprites.json @@ -53,10 +53,10 @@ { "costumeName": "abby-d", "baseLayerID": -1, - "baseLayerMD5": "2193cf08f74b8354f7c4fac37a06ea24.svg", + "baseLayerMD5": "2d05d4da65e7b5009a491d8153d37253.svg", "bitmapResolution": 1, - "rotationCenterX": 31, - "rotationCenterY": 100 + "rotationCenterX": 32, + "rotationCenterY": 101 } ], "currentCostumeIndex": 0, @@ -118,7 +118,7 @@ }, { "name": "Andie", - "md5": "936abdaaac3a3f7d27e5f30b3da00e5f.svg", + "md5": "419ff5a76adbb4b076c072f68267fdeb.svg", "type": "sprite", "tags": [ "sports", @@ -138,46 +138,46 @@ "objName": "Andie", "sounds": [ { - "soundName": "pop", + "soundName": "basketball bounce", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1727f65b5f22d151685b8e5917456a60.wav", + "sampleCount": 8099, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "andie-a", - "baseLayerID": -1, - "baseLayerMD5": "936abdaaac3a3f7d27e5f30b3da00e5f.svg", + "baseLayerID": 0, + "baseLayerMD5": "419ff5a76adbb4b076c072f68267fdeb.svg", "bitmapResolution": 1, "rotationCenterX": 86, - "rotationCenterY": 77 + "rotationCenterY": 81 }, { "costumeName": "andie-b", "baseLayerID": -1, - "baseLayerMD5": "cc2953492b40e231fdee3d8c9994a646.svg", + "baseLayerMD5": "a6a766912b4e43064ee3f389c72925b4.svg", "bitmapResolution": 1, "rotationCenterX": 46, - "rotationCenterY": 77 + "rotationCenterY": 106 }, { "costumeName": "andie-c", "baseLayerID": -1, - "baseLayerMD5": "76e26ccb79a94d3e89f67e64b4cb9731.svg", + "baseLayerMD5": "fac1025857d2c28391cb31f39a80df54.svg", "bitmapResolution": 1, "rotationCenterX": 52, - "rotationCenterY": 36 + "rotationCenterY": 65 }, { "costumeName": "andie-d", "baseLayerID": -1, - "baseLayerMD5": "80fa011244f3e3be56b96ebcb9e40d59.svg", + "baseLayerMD5": "7af50835e05d43e5e07cd2d641e7401e.svg", "bitmapResolution": 1, - "rotationCenterX": 86, - "rotationCenterY": 43 + "rotationCenterX": 87, + "rotationCenterY": 72 } ], "currentCostumeIndex": 0, @@ -193,7 +193,7 @@ }, { "name": "Anina Dance", - "md5": "b66762519d9542c0ab4fe754b47fe593.png", + "md5": "84c5e22b4303c7c1fb707125706c9aaa.png", "type": "sprite", "tags": [ "people", @@ -202,7 +202,7 @@ "info": [ 0, 13, - 1 + 2 ], "json": { "objName": "Anina Dance", @@ -214,13 +214,21 @@ "sampleCount": 176401, "rate": 22050, "format": "adpcm" + }, + { + "soundName": "dance magic", + "soundID": -1, + "md5": "042309f190183383c0b1c1fc3edc2e84.wav", + "sampleCount": 187200, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "anina stance", - "baseLayerID": 0, - "baseLayerMD5": "b66762519d9542c0ab4fe754b47fe593.png", + "baseLayerID": -1, + "baseLayerMD5": "84c5e22b4303c7c1fb707125706c9aaa.png", "bitmapResolution": 2, "rotationCenterX": 76, "rotationCenterY": 252 @@ -251,7 +259,7 @@ }, { "costumeName": "anina top freeze", - "baseLayerID": 1, + "baseLayerID": -1, "baseLayerMD5": "b7693bd6250d4411ee622b67f8025924.png", "bitmapResolution": 2, "rotationCenterX": 110, @@ -259,7 +267,7 @@ }, { "costumeName": "anina R cross", - "baseLayerID": 2, + "baseLayerID": -1, "baseLayerMD5": "3948aad16f8169c013c956dd152a09a6.png", "bitmapResolution": 2, "rotationCenterX": 126, @@ -267,7 +275,7 @@ }, { "costumeName": "anina pop front", - "baseLayerID": 3, + "baseLayerID": -1, "baseLayerMD5": "4931a363e3e4efa20230f6ff2991c6b4.png", "bitmapResolution": 2, "rotationCenterX": 68, @@ -352,10 +360,10 @@ "objName": "Apple", "sounds": [ { - "soundName": "pop", + "soundName": "Chomp", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, + "md5": "0b1e3033140d094563248e61de4039e5.wav", + "sampleCount": 2912, "rate": 11025, "format": "" } @@ -595,13 +603,21 @@ "info": [ 0, 5, - 1 + 2 ], "json": { "objName": "Ball", "sounds": [ { - "soundName": "pop", + "soundName": "Boing", + "soundID": -1, + "md5": "53a3c2e27d1fb5fdb14aaf0cb41e7889.wav", + "sampleCount": 6804, + "rate": 22050, + "format": "adpcm" + }, + { + "soundName": "Pop", "soundID": -1, "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", "sampleCount": 258, @@ -755,7 +771,7 @@ "objName": "Balloon1", "sounds": [ { - "soundName": "pop", + "soundName": "Pop", "soundID": -1, "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", "sampleCount": 258, @@ -800,9 +816,66 @@ "spriteInfo": {} } }, + { + "name": "Bananas", + "md5": "1a76b68e056954f5c8ac1f073df1533d.svg", + "type": "sprite", + "tags": [ + "food", + "yellow", + "mushy", + "potassium", + "fruit" + ], + "info": [ + 0, + 1, + 2 + ], + "json": { + "objName": "Bananas", + "sounds": [ + { + "soundName": "Chomp", + "soundID": -1, + "md5": "0b1e3033140d094563248e61de4039e5.wav", + "sampleCount": 2912, + "rate": 11025, + "format": "" + }, + { + "soundName": "Bite", + "soundID": -1, + "md5": "0039635b1d6853face36581784558454.wav", + "sampleCount": 7672, + "rate": 22050, + "format": "adpcm" + } + ], + "costumes": [ + { + "costumeName": "bananas", + "baseLayerID": -1, + "baseLayerMD5": "1a76b68e056954f5c8ac1f073df1533d.svg", + "bitmapResolution": 1, + "rotationCenterX": 39, + "rotationCenterY": 38 + } + ], + "currentCostumeIndex": 0, + "scratchX": 83, + "scratchY": -45, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, { "name": "Baseball", - "md5": "ae05a2a1fc22ce5929b8a67f695bfc9c.svg", + "md5": "838dc2b2278619d3a9beefc6c8072a08.svg", "type": "sprite", "tags": [ "baseball", @@ -831,7 +904,7 @@ { "costumeName": "baseball", "baseLayerID": -1, - "baseLayerMD5": "ae05a2a1fc22ce5929b8a67f695bfc9c.svg", + "baseLayerMD5": "838dc2b2278619d3a9beefc6c8072a08.svg", "bitmapResolution": 1, "rotationCenterX": 28, "rotationCenterY": 28 @@ -850,7 +923,7 @@ }, { "name": "Basketball", - "md5": "b15c425f3eef68e7d095ee91321cb52a.svg", + "md5": "482f04cbffa835c4abf29ab996d3fbbd.svg", "type": "sprite", "tags": [ "sports", @@ -860,7 +933,7 @@ "info": [ 0, 1, - 1 + 2 ], "json": { "objName": "Basketball", @@ -872,16 +945,24 @@ "sampleCount": 258, "rate": 11025, "format": "" + }, + { + "soundName": "basketball bounce", + "soundID": -1, + "md5": "1727f65b5f22d151685b8e5917456a60.wav", + "sampleCount": 8099, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "basketball", "baseLayerID": -1, - "baseLayerMD5": "b15c425f3eef68e7d095ee91321cb52a.svg", + "baseLayerMD5": "482f04cbffa835c4abf29ab996d3fbbd.svg", "bitmapResolution": 1, - "rotationCenterX": 26, - "rotationCenterY": 26 + "rotationCenterX": 23, + "rotationCenterY": 23 } ], "currentCostumeIndex": 0, @@ -897,7 +978,7 @@ }, { "name": "Bat", - "md5": "3b6274510488d5b26447c1c266475801.svg", + "md5": "75bf9a5c5ec7dc9325a0301b6f6e72dd.svg", "type": "sprite", "tags": [ "fantasy", @@ -916,10 +997,10 @@ "objName": "Bat", "sounds": [ { - "soundName": "pop", + "soundName": "owl", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, + "md5": "e8b6d605f5a1bb36c29e4e21ef754209.wav", + "sampleCount": 8111, "rate": 11025, "format": "" } @@ -928,39 +1009,39 @@ { "costumeName": "bat-a", "baseLayerID": -1, - "baseLayerMD5": "3b6274510488d5b26447c1c266475801.svg", + "baseLayerMD5": "75bf9a5c5ec7dc9325a0301b6f6e72dd.svg", "bitmapResolution": 1, - "rotationCenterX": 65, - "rotationCenterY": 61 + "rotationCenterX": 80, + "rotationCenterY": 60 }, { "costumeName": "bat-b", "baseLayerID": -1, - "baseLayerMD5": "8b20987d450379a445cbfc41ff2a3874.svg", + "baseLayerMD5": "b92d4967a0c233b0e78d6e834653f846.svg", "bitmapResolution": 1, - "rotationCenterX": 65, + "rotationCenterX": 39, "rotationCenterY": 61 }, { "costumeName": "bat-c", "baseLayerID": -1, - "baseLayerMD5": "c6d6a6892638e489902fbba803050de1.svg", + "baseLayerMD5": "56b1458bd81b8ac2401c216f7352343c.svg", "bitmapResolution": 1, - "rotationCenterX": 65, - "rotationCenterY": 61 + "rotationCenterX": 68, + "rotationCenterY": 66 }, { "costumeName": "bat-d", "baseLayerID": -1, - "baseLayerMD5": "e3da64838b2c74b6dd9a080b030e327e.svg", + "baseLayerMD5": "7f806f928a192aab0b0d03aa96a89a67.svg", "bitmapResolution": 1, - "rotationCenterX": 65, - "rotationCenterY": 61 + "rotationCenterX": 29, + "rotationCenterY": 62 } ], "currentCostumeIndex": 0, - "scratchX": -48, - "scratchY": 109, + "scratchX": -81, + "scratchY": 107, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -971,7 +1052,7 @@ }, { "name": "Batter", - "md5": "cc7510ea0a0e2f6eaf232f8ca73e3b53.svg", + "md5": "f7357c4cef5f31c6460e4f69dd0adabb.svg", "type": "sprite", "tags": [ "baseball", @@ -1000,39 +1081,39 @@ { "costumeName": "batter-a", "baseLayerID": -1, - "baseLayerMD5": "cc7510ea0a0e2f6eaf232f8ca73e3b53.svg", + "baseLayerMD5": "f7357c4cef5f31c6460e4f69dd0adabb.svg", "bitmapResolution": 1, - "rotationCenterX": 44, - "rotationCenterY": 71 + "rotationCenterX": 46, + "rotationCenterY": 80 }, { "costumeName": "batter-b", "baseLayerID": -1, - "baseLayerMD5": "7e7f9ce18fd84e0e95a96ad30fb58c8c.svg", + "baseLayerMD5": "c60fe1da0dcddedfd152384c60cbd5cc.svg", "bitmapResolution": 1, - "rotationCenterX": 14, - "rotationCenterY": 58 + "rotationCenterX": 16, + "rotationCenterY": 67 }, { "costumeName": "batter-c", "baseLayerID": -1, - "baseLayerMD5": "05cc730290919069b8d08447214c7293.svg", + "baseLayerMD5": "165c30e123e785d4cbbc0d9621f1c349.svg", "bitmapResolution": 1, - "rotationCenterX": 92, - "rotationCenterY": 57 + "rotationCenterX": 94, + "rotationCenterY": 66 }, { "costumeName": "batter-d", "baseLayerID": -1, - "baseLayerMD5": "24c32bf9bf066e36e49beb03ccd3db8f.svg", + "baseLayerMD5": "e82529d9c31b56a3dd098ccc94be68a6.svg", "bitmapResolution": 1, - "rotationCenterX": 64, - "rotationCenterY": 95 + "rotationCenterX": 70, + "rotationCenterY": 102 } ], "currentCostumeIndex": 0, - "scratchX": 68, - "scratchY": 75, + "scratchX": 121, + "scratchY": 70, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -1054,11 +1135,19 @@ "info": [ 0, 1, - 1 + 2 ], "json": { "objName": "Beachball", "sounds": [ + { + "soundName": "Basketball Bounce", + "soundID": -1, + "md5": "1727f65b5f22d151685b8e5917456a60.wav", + "sampleCount": 8099, + "rate": 22050, + "format": "adpcm" + }, { "soundName": "pop", "soundID": -1, @@ -1127,10 +1216,10 @@ { "costumeName": "bear-b", "baseLayerID": -1, - "baseLayerMD5": "d6517131718621a7aae8fc4f27de1ded.svg", + "baseLayerMD5": "92a64ce184c8515217e0e98e49624622.svg", "bitmapResolution": 1, - "rotationCenterX": 80, - "rotationCenterY": 140 + "rotationCenterX": 94, + "rotationCenterY": 191 } ], "currentCostumeIndex": 0, @@ -1353,7 +1442,7 @@ }, { "name": "Ben", - "md5": "7f32d8d78ad64f50c018b7b578de2e18.svg", + "md5": "4386d05d5abd6e1aa0a93b7b447d1b39.svg", "type": "sprite", "tags": [ "sports", @@ -1365,7 +1454,7 @@ "info": [ 0, 4, - 1 + 2 ], "json": { "objName": "Ben", @@ -1377,40 +1466,48 @@ "sampleCount": 258, "rate": 11025, "format": "" + }, + { + "soundName": "basketball bounce", + "soundID": -1, + "md5": "1727f65b5f22d151685b8e5917456a60.wav", + "sampleCount": 8099, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "ben-a", "baseLayerID": -1, - "baseLayerMD5": "7f32d8d78ad64f50c018b7b578de2e18.svg", + "baseLayerMD5": "4386d05d5abd6e1aa0a93b7b447d1b39.svg", "bitmapResolution": 1, - "rotationCenterX": 76, - "rotationCenterY": 75 + "rotationCenterX": 53, + "rotationCenterY": 69 }, { "costumeName": "ben-b", "baseLayerID": -1, - "baseLayerMD5": "fb012e5d1baf80d33ae95fba3511151a.svg", + "baseLayerMD5": "90db137cd0743204d02fdc9be1c8eb94.svg", "bitmapResolution": 1, - "rotationCenterX": 76, - "rotationCenterY": 75 + "rotationCenterX": 53, + "rotationCenterY": 67 }, { "costumeName": "ben-c", "baseLayerID": -1, - "baseLayerMD5": "be0b1397965cf8ff2c4cecb84795138a.svg", + "baseLayerMD5": "5f05d41ede773b3bb379286e86a5b869.svg", "bitmapResolution": 1, - "rotationCenterX": 76, - "rotationCenterY": 75 + "rotationCenterX": 71, + "rotationCenterY": 72 }, { "costumeName": "ben-d", "baseLayerID": -1, - "baseLayerMD5": "e2aefdb538ebbb24e1ab1464f75ef134.svg", + "baseLayerMD5": "6f6e246567c0639c758ed4f6930be76f.svg", "bitmapResolution": 1, - "rotationCenterX": 76, - "rotationCenterY": 75 + "rotationCenterX": 42, + "rotationCenterY": 72 } ], "currentCostumeIndex": 0, @@ -1472,14 +1569,16 @@ }, { "name": "Bowtie", - "md5": "534d9924d2f9bfe240f041e3ce55ccf5.svg", + "md5": "7d876f3ba4b04f3193a701ed30b4490c.svg", "type": "sprite", "tags": [ - "fashion" + "fashion", + "clothing", + "bowtie" ], "info": [ 0, - 2, + 1, 1 ], "json": { @@ -1496,65 +1595,12 @@ ], "costumes": [ { - "costumeName": "bowtie-a", + "costumeName": "bowtie", "baseLayerID": -1, - "baseLayerMD5": "534d9924d2f9bfe240f041e3ce55ccf5.svg", + "baseLayerMD5": "7d876f3ba4b04f3193a701ed30b4490c.svg", "bitmapResolution": 1, - "rotationCenterX": 11, - "rotationCenterY": 4 - }, - { - "costumeName": "bowtie-b", - "baseLayerID": -1, - "baseLayerMD5": "8be1e90e19cd1faced8a2e83c2b5c90d.svg", - "bitmapResolution": 1, - "rotationCenterX": 11, - "rotationCenterY": 4 - } - ], - "currentCostumeIndex": 0, - "scratchX": -66, - "scratchY": 0, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, - { - "name": "Bowtie1", - "md5": "aebd82272a5f4a1dd3c23d6e98537017.svg", - "type": "sprite", - "tags": [ - "fashion" - ], - "info": [ - 0, - 1, - 1 - ], - "json": { - "objName": "Bowtie1", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "bowtie-01", - "baseLayerID": -1, - "baseLayerMD5": "aebd82272a5f4a1dd3c23d6e98537017.svg", - "bitmapResolution": 1, - "rotationCenterX": 13, - "rotationCenterY": 7 + "rotationCenterX": 15, + "rotationCenterY": 8 } ], "currentCostumeIndex": 0, @@ -1837,8 +1883,8 @@ } ], "currentCostumeIndex": 0, - "scratchX": -130, - "scratchY": 77, + "scratchX": -107, + "scratchY": 104, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -2199,7 +2245,7 @@ "objName": "Cake", "sounds": [ { - "soundName": "birthday", + "soundName": "Birthday", "soundID": -1, "md5": "89691587a169d935a58c48c3d4e78534.wav", "sampleCount": 161408, @@ -2252,12 +2298,12 @@ "objName": "Calvrett", "sounds": [ { - "soundName": "pop", + "soundName": "dance chill out", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "b235da45581b1f212c9e9cce70d2a2dc.wav", + "sampleCount": 222822, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -2291,7 +2337,7 @@ }, { "name": "Casey", - "md5": "30a4dafa852311b2a07d72e1bb060326.svg", + "md5": "b7269d70079e96299666cc7b3f8eabc3.svg", "type": "sprite", "tags": [ "sports", @@ -2308,46 +2354,46 @@ "objName": "Casey", "sounds": [ { - "soundName": "pop", + "soundName": "basketball bounce", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1727f65b5f22d151685b8e5917456a60.wav", + "sampleCount": 8099, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "casey-a", "baseLayerID": -1, - "baseLayerMD5": "30a4dafa852311b2a07d72e1bb060326.svg", + "baseLayerMD5": "b7269d70079e96299666cc7b3f8eabc3.svg", "bitmapResolution": 1, - "rotationCenterX": 82, - "rotationCenterY": 72 + "rotationCenterX": 75, + "rotationCenterY": 62 }, { "costumeName": "casey-b", "baseLayerID": -1, - "baseLayerMD5": "c8c0a25bebac8b35b8eae7ddd716d061.svg", + "baseLayerMD5": "d22f6e818106f247f91e1c7c315863c3.svg", "bitmapResolution": 1, - "rotationCenterX": 82, - "rotationCenterY": 72 + "rotationCenterX": 60, + "rotationCenterY": 74 }, { "costumeName": "casey-c", "baseLayerID": -1, - "baseLayerMD5": "104d363c48c373384c6c80abbbbb0ad6.svg", + "baseLayerMD5": "a6ea02408a3f8366dfd704aae9a6ed0b.svg", "bitmapResolution": 1, - "rotationCenterX": 61, - "rotationCenterY": 61 + "rotationCenterX": 57, + "rotationCenterY": 72 }, { "costumeName": "casey-d", "baseLayerID": -1, - "baseLayerMD5": "3d4bddb908bf912b938c111bfa38c28a.svg", + "baseLayerMD5": "033ef301acfdfbc38774a14a559c1654.svg", "bitmapResolution": 1, - "rotationCenterX": 82, - "rotationCenterY": 72 + "rotationCenterX": 71, + "rotationCenterY": 74 } ], "currentCostumeIndex": 0, @@ -2378,12 +2424,12 @@ "objName": "Cassy Dance", "sounds": [ { - "soundName": "pop", + "soundName": "dance around", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "8bcea76415eaf98ec1cbc3825845b934.wav", + "sampleCount": 343746, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -2433,7 +2479,7 @@ }, { "name": "Cat", - "md5": "09dc888b0b7df19f70d81588ae73420e.svg", + "md5": "b7853f557e4426412e64bb3da6531a99.svg", "type": "sprite", "tags": [ "animals", @@ -2453,7 +2499,7 @@ "objName": "Cat", "sounds": [ { - "soundName": "meow", + "soundName": "Meow", "soundID": -1, "md5": "83c36d806dc92327b9e7049a565c6bff.wav", "sampleCount": 18688, @@ -2465,23 +2511,23 @@ { "costumeName": "cat-a", "baseLayerID": -1, - "baseLayerMD5": "09dc888b0b7df19f70d81588ae73420e.svg", + "baseLayerMD5": "b7853f557e4426412e64bb3da6531a99.svg", "bitmapResolution": 1, - "rotationCenterX": 47, - "rotationCenterY": 55 + "rotationCenterX": 48, + "rotationCenterY": 50 }, { "costumeName": "cat-b", "baseLayerID": -1, - "baseLayerMD5": "3696356a03a8d938318876a593572843.svg", + "baseLayerMD5": "e6ddc55a6ddd9cc9d84fe0b4c21e016f.svg", "bitmapResolution": 1, - "rotationCenterX": 47, - "rotationCenterY": 55 + "rotationCenterX": 46, + "rotationCenterY": 53 } ], "currentCostumeIndex": 0, - "scratchX": 0, - "scratchY": 0, + "scratchX": 24, + "scratchY": 16, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -2498,7 +2544,7 @@ "cat", "kitty", "kitten", - "animal", + "animals", "mammal" ], "info": [ @@ -2541,7 +2587,7 @@ }, { "name": "Cat Flying", - "md5": "1e81725d2d2c7de4a2dd4a145198a43c.svg", + "md5": "bf6b9f91e6928be67eda5154ca5fb2c6.svg", "type": "sprite", "tags": [ "animals", @@ -2558,7 +2604,7 @@ "objName": "Cat Flying", "sounds": [ { - "soundName": "pop", + "soundName": "Pop", "soundID": -1, "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", "sampleCount": 258, @@ -2570,18 +2616,18 @@ { "costumeName": "cat flying-a", "baseLayerID": -1, - "baseLayerMD5": "1e81725d2d2c7de4a2dd4a145198a43c.svg", + "baseLayerMD5": "bf6b9f91e6928be67eda5154ca5fb2c6.svg", "bitmapResolution": 1, - "rotationCenterX": 67, - "rotationCenterY": 48 + "rotationCenterX": 55, + "rotationCenterY": 37 }, { "costumeName": "cat flying-b", "baseLayerID": -1, - "baseLayerMD5": "0d192725870ef0eda50d91cab0e3c9c5.svg", + "baseLayerMD5": "6a5f707419e619463135f5bae599fa5b.svg", "bitmapResolution": 1, - "rotationCenterX": 42, - "rotationCenterY": 44 + "rotationCenterX": 44, + "rotationCenterY": 46 } ], "currentCostumeIndex": 0, @@ -2597,7 +2643,7 @@ }, { "name": "Catcher", - "md5": "6700cf1d4d653283161d2bad20f74d39.svg", + "md5": "27c88c54a59b9fff7e3e4763a6e9874e.svg", "type": "sprite", "tags": [ "baseball", @@ -2626,34 +2672,34 @@ { "costumeName": "catcher-a", "baseLayerID": -1, - "baseLayerMD5": "6700cf1d4d653283161d2bad20f74d39.svg", + "baseLayerMD5": "27c88c54a59b9fff7e3e4763a6e9874e.svg", "bitmapResolution": 1, - "rotationCenterX": 34, - "rotationCenterY": 33 + "rotationCenterX": 41, + "rotationCenterY": 51 }, { "costumeName": "catcher-b", - "baseLayerID": -1, - "baseLayerMD5": "d59c293e6fb4e15aa6bcfca5b1d0f2a6.svg", + "baseLayerID": 0, + "baseLayerMD5": "799a822cffc9460c0bf1b387cbd5e06b.svg", "bitmapResolution": 1, - "rotationCenterX": 34, - "rotationCenterY": 29 + "rotationCenterX": 41, + "rotationCenterY": 47 }, { "costumeName": "catcher-c", "baseLayerID": -1, - "baseLayerMD5": "55a2b54e5e414c9e2a17caf73ad06b87.svg", + "baseLayerMD5": "a6f2bf8b354e8d1a789f8e6f83133d02.svg", "bitmapResolution": 1, - "rotationCenterX": 53, - "rotationCenterY": 69 + "rotationCenterX": 60, + "rotationCenterY": 87 }, { "costumeName": "catcher-d", "baseLayerID": -1, - "baseLayerMD5": "d8943c5c2700e49f26f77826b9a3c238.svg", + "baseLayerMD5": "c86af45d9168bfb81259e5bb40be1778.svg", "bitmapResolution": 1, - "rotationCenterX": 79, - "rotationCenterY": 28 + "rotationCenterX": 86, + "rotationCenterY": 46 } ], "currentCostumeIndex": 0, @@ -2680,7 +2726,7 @@ "info": [ 0, 4, - 1 + 2 ], "json": { "objName": "Centaur", @@ -2692,6 +2738,14 @@ "sampleCount": 258, "rate": 11025, "format": "" + }, + { + "soundName": "snort", + "soundID": -1, + "md5": "362d7440a57cab29914fecea621e50d4.wav", + "sampleCount": 16421, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -2741,7 +2795,7 @@ }, { "name": "Champ99", - "md5": "1cb9d1a98cebc8a1d83af7633fdb9cd0.png", + "md5": "7b073f47fbd9421e0d60daacc157f506.png", "type": "sprite", "tags": [ "people", @@ -2756,19 +2810,19 @@ "objName": "Champ99", "sounds": [ { - "soundName": "pop", + "soundName": "dance celebrate", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "0edb8fb88af19e6e17d0f8cf64c1d136.wav", + "sampleCount": 176401, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "champ99-a", - "baseLayerID": 6, - "baseLayerMD5": "1cb9d1a98cebc8a1d83af7633fdb9cd0.png", + "baseLayerID": -1, + "baseLayerMD5": "7b073f47fbd9421e0d60daacc157f506.png", "bitmapResolution": 2, "rotationCenterX": 248, "rotationCenterY": 306 @@ -2880,7 +2934,7 @@ }, { "name": "Chick", - "md5": "0abf71fb210bbda7eac0bb6ecaab25de.svg", + "md5": "7cd74ad0da97a828e8499ee181c9dbb1.svg", "type": "sprite", "tags": [ "animals", @@ -2890,42 +2944,114 @@ ], "info": [ 0, - 2, + 3, 1 ], "json": { "objName": "Chick", "sounds": [ { - "soundName": "pop", + "soundName": "Chirp", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "3b8236bbb288019d93ae38362e865972.wav", + "sampleCount": 5301, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "chick-a", "baseLayerID": -1, - "baseLayerMD5": "0abf71fb210bbda7eac0bb6ecaab25de.svg", + "baseLayerMD5": "7cd74ad0da97a828e8499ee181c9dbb1.svg", "bitmapResolution": 1, - "rotationCenterX": 20, - "rotationCenterY": 21 + "rotationCenterX": 32, + "rotationCenterY": 37 }, { "costumeName": "chick-b", "baseLayerID": -1, - "baseLayerMD5": "3577aafabcfd6bcc7f7512be940a18b7.svg", + "baseLayerMD5": "07c2dad5f7063fddf7705f4817fad4f6.svg", "bitmapResolution": 1, - "rotationCenterX": 20, - "rotationCenterY": 21 + "rotationCenterX": 32, + "rotationCenterY": 37 + }, + { + "costumeName": "chick-c", + "baseLayerID": -1, + "baseLayerMD5": "272d3e4503a4123df62f25b67bad6f03.svg", + "bitmapResolution": 1, + "rotationCenterX": 21, + "rotationCenterY": 24 } ], "currentCostumeIndex": 0, - "scratchX": 20, - "scratchY": -108, + "scratchX": -1, + "scratchY": -122, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, + { + "name": "City Bus", + "md5": "accf64aceae7997b7533fb51624ca297.svg", + "type": "sprite", + "tags": [ + "city", + "bus", + "vehicle", + "car" + ], + "info": [ + 0, + 2, + 2 + ], + "json": { + "objName": "City Bus", + "sounds": [ + { + "soundName": "clown honk", + "soundID": -1, + "md5": "ec66961f188e9b8a9c75771db744d096.wav", + "sampleCount": 9009, + "rate": 22050, + "format": "adpcm" + }, + { + "soundName": "car vroom", + "soundID": -1, + "md5": "ead1da4a87ff6cb53441142f7ac37b8f.wav", + "sampleCount": 43358, + "rate": 22050, + "format": "adpcm" + } + ], + "costumes": [ + { + "costumeName": "City Bus-a", + "baseLayerID": -1, + "baseLayerMD5": "accf64aceae7997b7533fb51624ca297.svg", + "bitmapResolution": 1, + "rotationCenterX": 107, + "rotationCenterY": 67 + }, + { + "costumeName": "City Bus-b", + "baseLayerID": -1, + "baseLayerMD5": "b9ea0e326ffa98bc58a73bdf17c9542b.svg", + "bitmapResolution": 1, + "rotationCenterX": 107, + "rotationCenterY": 67 + } + ], + "currentCostumeIndex": 0, + "scratchX": 158, + "scratchY": -54, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -3100,11 +3226,12 @@ } }, { - "name": "Convertible3", + "name": "Convertible 2", "md5": "b6ac3c9e1789cba2302d2ef82d62d019.svg", "type": "sprite", "tags": [ "transportation", + "car", "things" ], "info": [ @@ -3113,7 +3240,7 @@ 1 ], "json": { - "objName": "Convertible3", + "objName": "Convertible 2", "sounds": [ { "soundName": "pop", @@ -3126,7 +3253,7 @@ ], "costumes": [ { - "costumeName": "convertible3", + "costumeName": "convertible 3", "baseLayerID": -1, "baseLayerMD5": "b6ac3c9e1789cba2302d2ef82d62d019.svg", "bitmapResolution": 1, @@ -3219,18 +3346,26 @@ "info": [ 0, 2, - 1 + 2 ], "json": { "objName": "Crystal", "sounds": [ { - "soundName": "pop", + "soundName": "Magic Spell", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1cb60ecdb1075c8769cb346d5c2a22c7.wav", + "sampleCount": 43077, + "rate": 22050, + "format": "adpcm" + }, + { + "soundName": "collect", + "soundID": -1, + "md5": "32514c51e03db680e9c63857b840ae78.wav", + "sampleCount": 13320, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -3290,7 +3425,7 @@ "costumes": [ { "costumeName": "dm stance", - "baseLayerID": 7, + "baseLayerID": -1, "baseLayerMD5": "dafbdfe454c5ec7029b5c1e07fcabc90.png", "bitmapResolution": 2, "rotationCenterX": 106, @@ -3420,12 +3555,12 @@ "objName": "Dan", "sounds": [ { - "soundName": "pop", + "soundName": "dance magic", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "042309f190183383c0b1c1fc3edc2e84.wav", + "sampleCount": 187200, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -3459,11 +3594,10 @@ }, { "name": "Dani", - "md5": "f3038fb0f4a00806b02081c6789dd8cf.svg", + "md5": "e620d1c3143b5ff05a0e4df89576e054.svg", "type": "sprite", "tags": [ - "people", - "fashion" + "people" ], "info": [ 0, @@ -3484,33 +3618,33 @@ ], "costumes": [ { - "costumeName": "dani-a", + "costumeName": "Dani-a", "baseLayerID": -1, - "baseLayerMD5": "f3038fb0f4a00806b02081c6789dd8cf.svg", + "baseLayerMD5": "e620d1c3143b5ff05a0e4df89576e054.svg", "bitmapResolution": 1, - "rotationCenterX": 49, - "rotationCenterY": 115 + "rotationCenterX": 37, + "rotationCenterY": 141 }, { - "costumeName": "dani-b", + "costumeName": "Dani-b", "baseLayerID": -1, - "baseLayerMD5": "e5c7dd4905a78e1d54087b7165dd1e8c.svg", + "baseLayerMD5": "4de54672f1c7af00623a0ee3bc490ba7.svg", "bitmapResolution": 1, - "rotationCenterX": 82, - "rotationCenterY": 115 + "rotationCenterX": 86, + "rotationCenterY": 141 }, { - "costumeName": "dani-c", + "costumeName": "Dani-c", "baseLayerID": -1, - "baseLayerMD5": "cbc5f9c67022af201d498bc9b35608b8.svg", + "baseLayerMD5": "b4a303342f9b7ed6a74059054347dd30.svg", "bitmapResolution": 1, - "rotationCenterX": 49, - "rotationCenterY": 113 + "rotationCenterX": 37, + "rotationCenterY": 138 } ], "currentCostumeIndex": 0, - "scratchX": -90, - "scratchY": -11, + "scratchX": -86, + "scratchY": -9, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -3667,7 +3801,7 @@ }, { "name": "Dinosaur1", - "md5": "75d367961807fff8e81f556da81dec24.svg", + "md5": "e70ab4de006f3df64e776abf912a62be.svg", "type": "sprite", "tags": [ "animals", @@ -3695,7 +3829,7 @@ { "costumeName": "dinosaur1-a", "baseLayerID": -1, - "baseLayerMD5": "75d367961807fff8e81f556da81dec24.svg", + "baseLayerMD5": "e70ab4de006f3df64e776abf912a62be.svg", "bitmapResolution": 1, "rotationCenterX": 98, "rotationCenterY": 92 @@ -3703,7 +3837,7 @@ { "costumeName": "dinosaur1-b", "baseLayerID": -1, - "baseLayerMD5": "ecdaee9c08ae68fd7a67f81302f00a97.svg", + "baseLayerMD5": "af2d8211a65dce9871d49fa70671ce0c.svg", "bitmapResolution": 1, "rotationCenterX": 98, "rotationCenterY": 47 @@ -3711,7 +3845,7 @@ { "costumeName": "dinosaur1-c", "baseLayerID": -1, - "baseLayerMD5": "02078a81abd2e10cb62ebcc853a40c92.svg", + "baseLayerMD5": "a2bb28d5ba55ba997bec63772437cecf.svg", "bitmapResolution": 1, "rotationCenterX": 81, "rotationCenterY": 91 @@ -3719,9 +3853,9 @@ { "costumeName": "dinosaur1-d", "baseLayerID": -1, - "baseLayerMD5": "c9ed031bc9bf11416142880f89436be9.svg", + "baseLayerMD5": "91f3dba45e8cec7bdff31ed5acde2c85.svg", "bitmapResolution": 1, - "rotationCenterX": 98, + "rotationCenterX": 99, "rotationCenterY": 91 } ], @@ -3738,7 +3872,7 @@ }, { "name": "Dinosaur2", - "md5": "5493f5deffe7aed451cd8b255740de4a.svg", + "md5": "f49719e8f2440f5d328f2604e97eb71d.svg", "type": "sprite", "tags": [ "animals", @@ -3767,34 +3901,34 @@ { "costumeName": "dinosaur2-a", "baseLayerID": -1, - "baseLayerMD5": "5493f5deffe7aed451cd8b255740de4a.svg", + "baseLayerMD5": "f49719e8f2440f5d328f2604e97eb71d.svg", "bitmapResolution": 1, - "rotationCenterX": 115, - "rotationCenterY": 72 + "rotationCenterX": 63, + "rotationCenterY": 67 }, { "costumeName": "dinosaur2-b", "baseLayerID": -1, - "baseLayerMD5": "70bba739b7df0bd08abb31026d078ee7.svg", + "baseLayerMD5": "1af6b3af3bf2cda862bbe01034a336bb.svg", "bitmapResolution": 1, - "rotationCenterX": 74, + "rotationCenterX": 76, "rotationCenterY": 67 }, { "costumeName": "dinosaur2-c", "baseLayerID": -1, - "baseLayerMD5": "4a51679d86aafcc9cee1c010fc141288.svg", + "baseLayerMD5": "e83b30a4517bbe155e2b4a6367c8f70b.svg", "bitmapResolution": 1, - "rotationCenterX": 62, + "rotationCenterX": 64, "rotationCenterY": 67 }, { "costumeName": "dinosaur2-d", "baseLayerID": -1, - "baseLayerMD5": "47053664449b24749aaf199925b19f8e.svg", + "baseLayerMD5": "8244f1fb159237179d1caaa69c070060.svg", "bitmapResolution": 1, - "rotationCenterX": 71, - "rotationCenterY": 66 + "rotationCenterX": 73, + "rotationCenterY": 67 } ], "currentCostumeIndex": 0, @@ -3810,7 +3944,7 @@ }, { "name": "Dinosaur3", - "md5": "17636db6f607c14a03a36e18abfea86a.svg", + "md5": "c4867c44f38df5bde472844878d5beea.svg", "type": "sprite", "tags": [ "animals", @@ -3840,42 +3974,42 @@ { "costumeName": "dinosaur3-a", "baseLayerID": -1, - "baseLayerMD5": "17636db6f607c14a03a36e18abfea86a.svg", + "baseLayerMD5": "c4867c44f38df5bde472844878d5beea.svg", "bitmapResolution": 1, - "rotationCenterX": 115, - "rotationCenterY": 72 + "rotationCenterX": 113, + "rotationCenterY": 42 }, { "costumeName": "dinosaur3-b", "baseLayerID": -1, - "baseLayerMD5": "1b20afc713b04ca5d01b25d050aa35ac.svg", + "baseLayerMD5": "b682c1e58ea1c6bea737670f57517b6f.svg", "bitmapResolution": 1, - "rotationCenterX": 115, - "rotationCenterY": 72 + "rotationCenterX": 113, + "rotationCenterY": 59 }, { "costumeName": "dinosaur3-c", "baseLayerID": -1, - "baseLayerMD5": "4700613077afa7c62659b3fd7d7c748e.svg", + "baseLayerMD5": "47a617ca48f4782efeeb60199cff801e.svg", "bitmapResolution": 1, - "rotationCenterX": 115, - "rotationCenterY": 72 + "rotationCenterX": 47, + "rotationCenterY": 59 }, { "costumeName": "dinosaur3-d", "baseLayerID": -1, - "baseLayerMD5": "a03f110ed12b73acc9bd84037fd6ae96.svg", + "baseLayerMD5": "710ff3380d6872b04785db97dc0565c6.svg", "bitmapResolution": 1, - "rotationCenterX": 115, - "rotationCenterY": 72 + "rotationCenterX": 113, + "rotationCenterY": 65 }, { "costumeName": "dinosaur3-e", "baseLayerID": -1, - "baseLayerMD5": "00e24e40535a1a621fee0f70895b2b61.svg", + "baseLayerMD5": "6f40025c1157f37858adf7fa85090e64.svg", "bitmapResolution": 1, - "rotationCenterX": 115, - "rotationCenterY": 72 + "rotationCenterX": 41, + "rotationCenterY": 40 } ], "currentCostumeIndex": 0, @@ -3891,7 +4025,7 @@ }, { "name": "Dinosaur4", - "md5": "9da591f8a6da251c800adb12a02c43cb.svg", + "md5": "d87b5e3c45f5d234c7aa47107c788f18.svg", "type": "sprite", "tags": [ "animals", @@ -3921,35 +4055,35 @@ "costumes": [ { "costumeName": "dinosaur4-a", - "baseLayerID": -1, - "baseLayerMD5": "9da591f8a6da251c800adb12a02c43cb.svg", + "baseLayerID": 0, + "baseLayerMD5": "d87b5e3c45f5d234c7aa47107c788f18.svg", "bitmapResolution": 1, - "rotationCenterX": 59, - "rotationCenterY": 54 + "rotationCenterX": 60, + "rotationCenterY": 52 }, { "costumeName": "dinosaur4-b", "baseLayerID": -1, - "baseLayerMD5": "a3028e87caeb8338f50b2c6dec9f23d2.svg", + "baseLayerMD5": "cfe906702bbd30bc3bb8acb53079b6b0.svg", "bitmapResolution": 1, - "rotationCenterX": 86, - "rotationCenterY": 54 + "rotationCenterX": 87, + "rotationCenterY": 52 }, { "costumeName": "dinosaur4-c", "baseLayerID": -1, - "baseLayerMD5": "9a4bbc1b104c8112be82c252a6ecfd11.svg", + "baseLayerMD5": "1879b07cfe788b0258afada17aa46973.svg", "bitmapResolution": 1, "rotationCenterX": 59, - "rotationCenterY": 54 + "rotationCenterY": 52 }, { "costumeName": "dinosaur4-d", "baseLayerID": -1, - "baseLayerMD5": "45061ff84a25723625d04f0476687633.svg", + "baseLayerMD5": "52c17657698ab82881ed3ccf83080684.svg", "bitmapResolution": 1, "rotationCenterX": 89, - "rotationCenterY": 90 + "rotationCenterY": 88 } ], "currentCostumeIndex": 0, @@ -3963,6 +4097,117 @@ "spriteInfo": {} } }, + { + "name": "Dinosaur5", + "md5": "42e3bf118c775ba54239af4276800a0a.png", + "type": "sprite", + "tags": [ + "animals", + "dinosaur", + "leigh mcg" + ], + "info": [ + 0, + 8, + 2 + ], + "json": { + "objName": "Dinosaur5", + "sounds": [ + { + "soundName": "dance funky", + "soundID": -1, + "md5": "51c00a150d33c4639203184bb24c637b.wav", + "sampleCount": 111870, + "rate": 22050, + "format": "adpcm" + }, + { + "soundName": "bite", + "soundID": -1, + "md5": "6759a83e9b92cd6082b68611e858fd23.wav", + "sampleCount": 8136, + "rate": 22050, + "format": "adpcm" + } + ], + "costumes": [ + { + "costumeName": "Dinosaur5-a", + "baseLayerID": -1, + "baseLayerMD5": "42e3bf118c775ba54239af4276800a0a.png", + "bitmapResolution": 2, + "rotationCenterX": 104, + "rotationCenterY": 150 + }, + { + "costumeName": "Dinosaur5-b", + "baseLayerID": -1, + "baseLayerMD5": "5a0832162a0cfa7adab6090c42e89714.png", + "bitmapResolution": 2, + "rotationCenterX": 112, + "rotationCenterY": 166 + }, + { + "costumeName": "Dinosaur5-c", + "baseLayerID": -1, + "baseLayerMD5": "26fca11e4251d60ed7aa5d08f4ae2a69.png", + "bitmapResolution": 2, + "rotationCenterX": 112, + "rotationCenterY": 150 + }, + { + "costumeName": "Dinosaur5-d", + "baseLayerID": -1, + "baseLayerMD5": "c4044a3badea77ced4f2db69aff866ed.png", + "bitmapResolution": 2, + "rotationCenterX": 90, + "rotationCenterY": 134 + }, + { + "costumeName": "Dinosaur5-e", + "baseLayerID": -1, + "baseLayerMD5": "f49b3b098a24474f20c8f4686681c611.png", + "bitmapResolution": 2, + "rotationCenterX": 88, + "rotationCenterY": 150 + }, + { + "costumeName": "Dinosaur5-f", + "baseLayerID": -1, + "baseLayerMD5": "9d200a7c2e93eac8cf52ede3a87d7969.png", + "bitmapResolution": 2, + "rotationCenterX": 94, + "rotationCenterY": 166 + }, + { + "costumeName": "Dinosaur5-g", + "baseLayerID": -1, + "baseLayerMD5": "5882227a9e2f0f3b2014c49328969762.png", + "bitmapResolution": 2, + "rotationCenterX": 102, + "rotationCenterY": 150 + }, + { + "costumeName": "Dinosaur5-h", + "baseLayerID": -1, + "baseLayerMD5": "3b2cf97b1cc7fc535162ba5849a0e29c.png", + "bitmapResolution": 2, + "rotationCenterX": 108, + "rotationCenterY": 134 + } + ], + "currentCostumeIndex": 0, + "scratchX": 0, + "scratchY": 0, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, { "name": "Diver1", "md5": "853803d5600b66538474909c5438c8ee.svg", @@ -4200,16 +4445,24 @@ "info": [ 0, 1, - 1 + 2 ], "json": { "objName": "Donut", "sounds": [ { - "soundName": "pop", + "soundName": "Bite", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, + "md5": "0039635b1d6853face36581784558454.wav", + "sampleCount": 7672, + "rate": 22050, + "format": "adpcm" + }, + { + "soundName": "Chomp", + "soundID": -1, + "md5": "0b1e3033140d094563248e61de4039e5.wav", + "sampleCount": 2912, "rate": 11025, "format": "" } @@ -4237,7 +4490,7 @@ }, { "name": "Dorian", - "md5": "b042b1a5fde03dd5abbc2f3f78d11a2c.svg", + "md5": "503f1d4d0fe61d071e1dd8b5233cceb0.svg", "type": "sprite", "tags": [ "sports", @@ -4254,46 +4507,46 @@ "objName": "Dorian", "sounds": [ { - "soundName": "pop", + "soundName": "basketball bounce", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1727f65b5f22d151685b8e5917456a60.wav", + "sampleCount": 8099, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "dorian-a", "baseLayerID": -1, - "baseLayerMD5": "b042b1a5fde03dd5abbc2f3f78d11a2c.svg", + "baseLayerMD5": "503f1d4d0fe61d071e1dd8b5233cceb0.svg", "bitmapResolution": 1, - "rotationCenterX": 82, - "rotationCenterY": 72 + "rotationCenterX": 60, + "rotationCenterY": 65 }, { "costumeName": "dorian-b", "baseLayerID": -1, - "baseLayerMD5": "d0b58b672606601ecfa3a406b537fa10.svg", + "baseLayerMD5": "d5f795ef2395bcd11ceee0b0c24c9341.svg", "bitmapResolution": 1, - "rotationCenterX": 82, - "rotationCenterY": 72 + "rotationCenterX": 60, + "rotationCenterY": 65 }, { "costumeName": "dorian-c", "baseLayerID": -1, - "baseLayerMD5": "445e461c73a5920098764a4cbad5bfe0.svg", + "baseLayerMD5": "4ad4ddf4251b9b5bcb16d8d77213685f.svg", "bitmapResolution": 1, - "rotationCenterX": 82, - "rotationCenterY": 72 + "rotationCenterX": 77, + "rotationCenterY": 67 }, { "costumeName": "dorian-d", "baseLayerID": -1, - "baseLayerMD5": "ba93ede3bbf75c0f707b0fb982bc27e8.svg", + "baseLayerMD5": "67fba4e187ecf42749bdd44e64f29192.svg", "bitmapResolution": 1, - "rotationCenterX": 82, - "rotationCenterY": 72 + "rotationCenterX": 77, + "rotationCenterY": 53 } ], "currentCostumeIndex": 0, @@ -4309,11 +4562,12 @@ }, { "name": "Dot", - "md5": "47c975e37f9a89c01d0d4d6fd17ef847.svg", + "md5": "3b9e6830365aeadb63c21b681777b783.svg", "type": "sprite", "tags": [ "space", "dog", + "animals", "wren mcdonald" ], "info": [ @@ -4337,34 +4591,34 @@ { "costumeName": "dot-a", "baseLayerID": -1, - "baseLayerMD5": "47c975e37f9a89c01d0d4d6fd17ef847.svg", + "baseLayerMD5": "3b9e6830365aeadb63c21b681777b783.svg", "bitmapResolution": 1, "rotationCenterX": 52, - "rotationCenterY": 69 + "rotationCenterY": 67 }, { "costumeName": "dot-b", "baseLayerID": -1, - "baseLayerMD5": "a9b7d5f7afa0c69c4044a3f541b9ab90.svg", + "baseLayerMD5": "a80a07906ffbb9671dba3633223859af.svg", "bitmapResolution": 1, "rotationCenterX": 65, - "rotationCenterY": 69 + "rotationCenterY": 67 }, { "costumeName": "dot-c", "baseLayerID": -1, - "baseLayerMD5": "5b7967159c9b83b0d0ed4f051ec2c9e9.svg", + "baseLayerMD5": "1e1bdccbab5b566beb7f91b5ec01352f.svg", "bitmapResolution": 1, - "rotationCenterX": 49, - "rotationCenterY": 70 + "rotationCenterX": 51, + "rotationCenterY": 69 }, { "costumeName": "dot-d", "baseLayerID": -1, - "baseLayerMD5": "23ffa385654304e4cac454390dde3606.svg", + "baseLayerMD5": "06d70d5de0e25f97c1c606e513ab019c.svg", "bitmapResolution": 1, - "rotationCenterX": 59, - "rotationCenterY": 69 + "rotationCenterX": 57, + "rotationCenterY": 67 } ], "currentCostumeIndex": 0, @@ -4452,12 +4706,12 @@ "objName": "Dragon", "sounds": [ { - "soundName": "pop", + "soundName": "Magic Spell", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1cb60ecdb1075c8769cb346d5c2a22c7.wav", + "sampleCount": 43077, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -4498,11 +4752,70 @@ } }, { - "name": "Dress", - "md5": "308b6ad8e6cfe5c2b2c1e3a1eb9cc404.svg", + "name": "Dragonfly", + "md5": "87aaecd2fb73580e6f1f38c5a2e579a9.svg", "type": "sprite", "tags": [ - "fashion" + "animals", + "insect", + "dragonfly", + "wetland", + "owen davey" + ], + "info": [ + 0, + 2, + 1 + ], + "json": { + "objName": "Dragonfly", + "sounds": [ + { + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + } + ], + "costumes": [ + { + "costumeName": "Dragonfly-a", + "baseLayerID": -1, + "baseLayerMD5": "87aaecd2fb73580e6f1f38c5a2e579a9.svg", + "bitmapResolution": 1, + "rotationCenterX": 108, + "rotationCenterY": 52 + }, + { + "costumeName": "Dragonfly-b", + "baseLayerID": -1, + "baseLayerMD5": "4e775a49bd5a7a6d38392731924f72e0.svg", + "bitmapResolution": 1, + "rotationCenterX": 108, + "rotationCenterY": 46 + } + ], + "currentCostumeIndex": 0, + "scratchX": 138, + "scratchY": 107, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, + { + "name": "Dress", + "md5": "fa7e0b37a324c212d0f1ce0f701a1ae0.svg", + "type": "sprite", + "tags": [ + "fashion", + "dress", + "clothing" ], "info": [ 0, @@ -4523,7 +4836,15 @@ ], "costumes": [ { - "costumeName": "dress-01", + "costumeName": "dress-a", + "baseLayerID": -1, + "baseLayerMD5": "fa7e0b37a324c212d0f1ce0f701a1ae0.svg", + "bitmapResolution": 1, + "rotationCenterX": 57, + "rotationCenterY": 83 + }, + { + "costumeName": "dress-b", "baseLayerID": -1, "baseLayerMD5": "308b6ad8e6cfe5c2b2c1e3a1eb9cc404.svg", "bitmapResolution": 1, @@ -4531,20 +4852,12 @@ "rotationCenterY": 83 }, { - "costumeName": "dress-02", + "costumeName": "dress-c", "baseLayerID": -1, "baseLayerMD5": "1f7a725ba285dc97cb142d4b812db624.svg", "bitmapResolution": 1, "rotationCenterX": 49, "rotationCenterY": 83 - }, - { - "costumeName": "dress-03", - "baseLayerID": -1, - "baseLayerMD5": "fa7e0b37a324c212d0f1ce0f701a1ae0.svg", - "bitmapResolution": 1, - "rotationCenterX": 57, - "rotationCenterY": 83 } ], "currentCostumeIndex": 0, @@ -4575,7 +4888,7 @@ "objName": "Drum", "sounds": [ { - "soundName": "high tom", + "soundName": "High Tom", "soundID": -1, "md5": "d623f99b3c8d33932eb2c6c9cfd817c5.wav", "sampleCount": 12320, @@ -4583,7 +4896,7 @@ "format": "" }, { - "soundName": "low tom", + "soundName": "Low Tom", "soundID": -1, "md5": "1569bbbd8952b0575e5a5cb5aefb50ba.wav", "sampleCount": 20000, @@ -4637,7 +4950,7 @@ "objName": "Drum Kit", "sounds": [ { - "soundName": "drum bass1", + "soundName": "Drum Bass1", "soundID": -1, "md5": "48328c874353617451e4c7902cc82817.wav", "sampleCount": 6528, @@ -4645,7 +4958,7 @@ "format": "" }, { - "soundName": "drum bass2", + "soundName": "Drum Bass2", "soundID": -1, "md5": "711a1270d1cf2e5de9b145ee539213e4.wav", "sampleCount": 3791, @@ -4653,7 +4966,7 @@ "format": "adpcm" }, { - "soundName": "drum bass3", + "soundName": "Drum Bass3", "soundID": -1, "md5": "c21704337b16359ea631b5f8eb48f765.wav", "sampleCount": 8576, @@ -4661,7 +4974,7 @@ "format": "" }, { - "soundName": "high tom", + "soundName": "High Tom", "soundID": -1, "md5": "d623f99b3c8d33932eb2c6c9cfd817c5.wav", "sampleCount": 12320, @@ -4669,7 +4982,7 @@ "format": "" }, { - "soundName": "low tom", + "soundName": "Low Tom", "soundID": -1, "md5": "1569bbbd8952b0575e5a5cb5aefb50ba.wav", "sampleCount": 20000, @@ -4840,7 +5153,7 @@ }, { "name": "Drum-snare", - "md5": "b0255be93e3c8be6ac687f4199a25c4b.svg", + "md5": "ace58710b8fa5fca8fb5bbca15e7d19c.svg", "type": "sprite", "tags": [ "music", @@ -4883,18 +5196,18 @@ { "costumeName": "drum-snare-a", "baseLayerID": -1, - "baseLayerMD5": "b0255be93e3c8be6ac687f4199a25c4b.svg", + "baseLayerMD5": "ace58710b8fa5fca8fb5bbca15e7d19c.svg", "bitmapResolution": 1, - "rotationCenterX": 23, - "rotationCenterY": 50 + "rotationCenterX": 21, + "rotationCenterY": 32 }, { "costumeName": "drum-snare-b", "baseLayerID": -1, - "baseLayerMD5": "f6d2f2a6e1055dab6262336625ddf652.svg", + "baseLayerMD5": "f6120839d3f73b262571add2444c443a.svg", "bitmapResolution": 1, - "rotationCenterX": 36, - "rotationCenterY": 66 + "rotationCenterX": 34, + "rotationCenterY": 48 } ], "currentCostumeIndex": 0, @@ -4908,6 +5221,168 @@ "spriteInfo": {} } }, + { + "name": "Drums Conga", + "md5": "b3da94523b6d3df2dd30602399599ab4.svg", + "type": "sprite", + "tags": [ + "drums", + "drum", + "music", + "instrument", + "percussion" + ], + "info": [ + 0, + 2, + 4 + ], + "json": { + "objName": "Drums Conga", + "sounds": [ + { + "soundName": "High Conga", + "soundID": -1, + "md5": "16144544de90e98a92a265d4fc3241ea.wav", + "sampleCount": 8192, + "rate": 22050, + "format": "" + }, + { + "soundName": "Low Conga", + "soundID": -1, + "md5": "0b6f94487cd8a1cf0bb77e15966656c3.wav", + "sampleCount": 8384, + "rate": 22050, + "format": "" + }, + { + "soundName": "Muted Conga", + "soundID": -1, + "md5": "1d4abbe3c9bfe198a88badb10762de75.wav", + "sampleCount": 4544, + "rate": 22050, + "format": "" + }, + { + "soundName": "Tap Conga", + "soundID": -1, + "md5": "fd9a67157f57f9cc6fe3cdce38a6d4a8.wav", + "sampleCount": 6880, + "rate": 22050, + "format": "" + } + ], + "costumes": [ + { + "costumeName": "Drums Conga-a", + "baseLayerID": -1, + "baseLayerMD5": "b3da94523b6d3df2dd30602399599ab4.svg", + "bitmapResolution": 1, + "rotationCenterX": 65, + "rotationCenterY": 66 + }, + { + "costumeName": "Drums Conga-b", + "baseLayerID": -1, + "baseLayerMD5": "037703c2b65f051c12e9b22dcf93856c.svg", + "bitmapResolution": 1, + "rotationCenterX": 66, + "rotationCenterY": 94 + } + ], + "currentCostumeIndex": 0, + "scratchX": 61, + "scratchY": -87, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, + { + "name": "Drums Tabla", + "md5": "68ce53b53fcc68744584c28d20144c4f.svg", + "type": "sprite", + "tags": [ + "drums", + "drum", + "music", + "instrument", + "percussion" + ], + "info": [ + 0, + 2, + 4 + ], + "json": { + "objName": "Drums Tabla", + "sounds": [ + { + "soundName": "Hi Na Tabla", + "soundID": -1, + "md5": "35b42d98c43404a5b1b52fb232a62bd7.wav", + "sampleCount": 4096, + "rate": 22050, + "format": "" + }, + { + "soundName": "Hi Tun Tabla", + "soundID": -1, + "md5": "da734693dfa6a9a7eccdc7f9a0ca9840.wav", + "sampleCount": 18656, + "rate": 22050, + "format": "" + }, + { + "soundName": "Lo Geh Tabla", + "soundID": -1, + "md5": "9205359ab69d042ed3da8a160a651690.wav", + "sampleCount": 30784, + "rate": 22050, + "format": "" + }, + { + "soundName": "Lo Gliss Tabla", + "soundID": -1, + "md5": "d7cd24689737569c93e7ea7344ba6b0e.wav", + "sampleCount": 7008, + "rate": 22050, + "format": "" + } + ], + "costumes": [ + { + "costumeName": "Tabla-a", + "baseLayerID": -1, + "baseLayerMD5": "68ce53b53fcc68744584c28d20144c4f.svg", + "bitmapResolution": 1, + "rotationCenterX": 79, + "rotationCenterY": 67 + }, + { + "costumeName": "Tabla-b", + "baseLayerID": -1, + "baseLayerMD5": "01b6ffb8691d32be10fabc77ddfb55b0.svg", + "bitmapResolution": 1, + "rotationCenterX": 86, + "rotationCenterY": 73 + } + ], + "currentCostumeIndex": 0, + "scratchX": -50, + "scratchY": -106, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, { "name": "Duck", "md5": "c3baf7eedfbdac8cd1e4f1f1f779dc0c.svg", @@ -4959,10 +5434,12 @@ }, { "name": "Earth", - "md5": "814197522984a302972998b1a7f92d91.svg", + "md5": "d9df2cf4f6eb2460d0de3e4f1e05f40b.svg", "type": "sprite", "tags": [ - "space" + "space", + "planet", + "earth" ], "info": [ 0, @@ -4983,12 +5460,12 @@ ], "costumes": [ { - "costumeName": "earth", + "costumeName": "Earth", "baseLayerID": -1, - "baseLayerMD5": "814197522984a302972998b1a7f92d91.svg", + "baseLayerMD5": "d9df2cf4f6eb2460d0de3e4f1e05f40b.svg", "bitmapResolution": 1, - "rotationCenterX": 72, - "rotationCenterY": 72 + "rotationCenterX": 41, + "rotationCenterY": 40 } ], "currentCostumeIndex": 0, @@ -5002,9 +5479,72 @@ "spriteInfo": {} } }, + { + "name": "Easel", + "md5": "edc81e35c94c07dc380c43ea9b311394.svg", + "type": "sprite", + "tags": [ + "bedroom", + "art", + "easel" + ], + "info": [ + 0, + 3, + 1 + ], + "json": { + "objName": "Easel", + "sounds": [ + { + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + } + ], + "costumes": [ + { + "costumeName": "Easel-a", + "baseLayerID": -1, + "baseLayerMD5": "edc81e35c94c07dc380c43ea9b311394.svg", + "bitmapResolution": 1, + "rotationCenterX": 69, + "rotationCenterY": 95 + }, + { + "costumeName": "Easel-b", + "baseLayerID": -1, + "baseLayerMD5": "cac798e8cf0364fa9a8bc52b85bda6b2.svg", + "bitmapResolution": 1, + "rotationCenterX": 69, + "rotationCenterY": 95 + }, + { + "costumeName": "Easel-c", + "baseLayerID": -1, + "baseLayerMD5": "afbf42eb496694afb31f9ea69b3a1a7a.svg", + "bitmapResolution": 1, + "rotationCenterX": 68, + "rotationCenterY": 94 + } + ], + "currentCostumeIndex": 0, + "scratchX": 182, + "scratchY": -41, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, { "name": "Egg", - "md5": "bc723738dfe626c5c3bb90970d985961.svg", + "md5": "6f1a24a63d16c45425aa2eb64934a278.svg", "type": "sprite", "tags": [ "food", @@ -5013,7 +5553,7 @@ ], "info": [ 0, - 5, + 6, 1 ], "json": { @@ -5032,47 +5572,55 @@ { "costumeName": "egg-a", "baseLayerID": -1, - "baseLayerMD5": "bc723738dfe626c5c3bb90970d985961.svg", + "baseLayerMD5": "6f1a24a63d16c45425aa2eb64934a278.svg", "bitmapResolution": 1, - "rotationCenterX": 68, - "rotationCenterY": 54 + "rotationCenterX": 18, + "rotationCenterY": 26 }, { "costumeName": "egg-b", "baseLayerID": -1, - "baseLayerMD5": "83016b7ff817f99be4a454600b4a78fc.svg", + "baseLayerMD5": "7f1320549dd8bfd596c52aec02d898ce.svg", "bitmapResolution": 1, - "rotationCenterX": 68, - "rotationCenterY": 54 + "rotationCenterX": 23, + "rotationCenterY": 27 }, { "costumeName": "egg-c", "baseLayerID": -1, - "baseLayerMD5": "d9a44d151fbd909bdbbcf7877055af6d.svg", + "baseLayerMD5": "62ed6f412aae07a575faa51451009168.svg", "bitmapResolution": 1, - "rotationCenterX": 68, - "rotationCenterY": 54 + "rotationCenterX": 28, + "rotationCenterY": 27 }, { "costumeName": "egg-d", "baseLayerID": -1, - "baseLayerMD5": "c91c7f72b8523b0910a84bce7d99c37b.svg", + "baseLayerMD5": "0dce0615471ebf1bebc3ba8b4bebbd18.svg", "bitmapResolution": 1, - "rotationCenterX": 68, - "rotationCenterY": 54 + "rotationCenterX": 19, + "rotationCenterY": 27 }, { "costumeName": "egg-e", "baseLayerID": -1, - "baseLayerMD5": "65c15516e62596e1f72e874359fc7254.svg", + "baseLayerMD5": "3848cf5708b7c712bb7bb9bbcce636aa.svg", "bitmapResolution": 1, - "rotationCenterX": 68, - "rotationCenterY": 54 + "rotationCenterX": 20, + "rotationCenterY": 26 + }, + { + "costumeName": "egg-f", + "baseLayerID": -1, + "baseLayerMD5": "0f7e63d9f23038a5012409af93c8dfad.svg", + "bitmapResolution": 1, + "rotationCenterX": 31, + "rotationCenterY": 26 } ], "currentCostumeIndex": 0, - "scratchX": -95, - "scratchY": 60, + "scratchX": -102, + "scratchY": 53, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -5155,12 +5703,12 @@ "objName": "Elf", "sounds": [ { - "soundName": "pop", + "soundName": "Magic Spell", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1cb60ecdb1075c8769cb346d5c2a22c7.wav", + "sampleCount": 43077, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -5216,49 +5764,6 @@ "spriteInfo": {} } }, - { - "name": "Empty", - "md5": "cd21514d0531fdffb22204e0ec5ed84a.svg", - "type": "sprite", - "tags": [], - "info": [ - 0, - 1, - 1 - ], - "json": { - "objName": "Empty", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "empty", - "baseLayerID": -1, - "baseLayerMD5": "cd21514d0531fdffb22204e0ec5ed84a.svg", - "bitmapResolution": 1, - "rotationCenterX": 0, - "rotationCenterY": 0 - } - ], - "currentCostumeIndex": 0, - "scratchX": 36, - "scratchY": 28, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, { "name": "Fairy", "md5": "fe97687c7f1b747bb6f41c252cc5926a.svg", @@ -5278,12 +5783,12 @@ "objName": "Fairy", "sounds": [ { - "soundName": "pop", + "soundName": "Magic Spell", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1cb60ecdb1075c8769cb346d5c2a22c7.wav", + "sampleCount": 43077, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -5353,18 +5858,26 @@ "info": [ 0, 4, - 1 + 2 ], "json": { "objName": "Fish", "sounds": [ { - "soundName": "pop", + "soundName": "bubbles", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, + "md5": "78b0be9c9c2f664158b886bc7e794095.wav", + "sampleCount": 45056, "rate": 11025, "format": "" + }, + { + "soundName": "ocean wave", + "soundID": -1, + "md5": "c904610d770398b98872a708a2f75611.wav", + "sampleCount": 99206, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -5412,6 +5925,143 @@ "spriteInfo": {} } }, + { + "name": "Fishbowl", + "md5": "9d1013e04ca7425f680a4b4e6dd6ede6.svg", + "type": "sprite", + "tags": [ + "fishbowl", + "fish", + "animals", + "pet" + ], + "info": [ + 0, + 2, + 1 + ], + "json": { + "objName": "Fishbowl", + "sounds": [ + { + "soundName": "water drop", + "soundID": -1, + "md5": "aa488de9e2c871e9d4faecd246ed737a.wav", + "sampleCount": 8136, + "rate": 22050, + "format": "adpcm" + } + ], + "costumes": [ + { + "costumeName": "Fishbowl-a", + "baseLayerID": -1, + "baseLayerMD5": "9d1013e04ca7425f680a4b4e6dd6ede6.svg", + "bitmapResolution": 1, + "rotationCenterX": 28, + "rotationCenterY": 24 + }, + { + "costumeName": "Fishbowl-b", + "baseLayerID": -1, + "baseLayerMD5": "0bbfc827e8d2f3da6db82efe95563f93.svg", + "bitmapResolution": 1, + "rotationCenterX": 28, + "rotationCenterY": 24 + } + ], + "currentCostumeIndex": 0, + "scratchX": -34, + "scratchY": 74, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, + { + "name": "Food Truck", + "md5": "b30ec8ff043e0f1747591d564d56d7fa.svg", + "type": "sprite", + "tags": [ + "city", + "truck", + "vehicle", + "food", + "car" + ], + "info": [ + 0, + 3, + 3 + ], + "json": { + "objName": "Food Truck", + "sounds": [ + { + "soundName": "car horn", + "soundID": -1, + "md5": "7c887f6a2ecd1cdb85d5527898d7f7a0.wav", + "sampleCount": 42443, + "rate": 22050, + "format": "adpcm" + }, + { + "soundName": "clown honk", + "soundID": -1, + "md5": "ec66961f188e9b8a9c75771db744d096.wav", + "sampleCount": 9009, + "rate": 22050, + "format": "adpcm" + }, + { + "soundName": "car vroom", + "soundID": -1, + "md5": "ead1da4a87ff6cb53441142f7ac37b8f.wav", + "sampleCount": 43358, + "rate": 22050, + "format": "adpcm" + } + ], + "costumes": [ + { + "costumeName": "Food Truck-a", + "baseLayerID": -1, + "baseLayerMD5": "b30ec8ff043e0f1747591d564d56d7fa.svg", + "bitmapResolution": 1, + "rotationCenterX": 121, + "rotationCenterY": 125 + }, + { + "costumeName": "Food Truck-b", + "baseLayerID": -1, + "baseLayerMD5": "afd51531621d2b33869b47342311b363.svg", + "bitmapResolution": 1, + "rotationCenterX": 121, + "rotationCenterY": 101 + }, + { + "costumeName": "Food Truck-c", + "baseLayerID": -1, + "baseLayerMD5": "42f88485a25bf9a683d28957db9f969f.svg", + "bitmapResolution": 1, + "rotationCenterX": 121, + "rotationCenterY": 92 + } + ], + "currentCostumeIndex": 0, + "scratchX": -50, + "scratchY": -54, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, { "name": "Football", "md5": "7ee31371b2eafba57cc5a78fc1a787fe.png", @@ -5513,7 +6163,7 @@ }, { "name": "Fox", - "md5": "fab5488e600e81565f0fc285fc7050f8.svg", + "md5": "0aef4633e8580e209020b054eb55982f.svg", "type": "sprite", "tags": [ "animals", @@ -5541,26 +6191,26 @@ { "costumeName": "fox-a", "baseLayerID": -1, - "baseLayerMD5": "fab5488e600e81565f0fc285fc7050f8.svg", + "baseLayerMD5": "0aef4633e8580e209020b054eb55982f.svg", "bitmapResolution": 1, - "rotationCenterX": 94, - "rotationCenterY": 54 + "rotationCenterX": 86, + "rotationCenterY": 44 }, { "costumeName": "fox-b", "baseLayerID": -1, - "baseLayerMD5": "afb192ae250a74dfac18bfc52d1d6266.svg", + "baseLayerMD5": "ae0bb1b900e853ddc8820e0c7bd60f53.svg", "bitmapResolution": 1, - "rotationCenterX": 94, - "rotationCenterY": 54 + "rotationCenterX": 44, + "rotationCenterY": 50 }, { "costumeName": "fox-c", "baseLayerID": -1, - "baseLayerMD5": "29f858d384db7998c0e5183f6a31a3b4.svg", + "baseLayerMD5": "3cc3761cbabdcf13dbfdf04048bfbccf.svg", "bitmapResolution": 1, - "rotationCenterX": 94, - "rotationCenterY": 54 + "rotationCenterX": 48, + "rotationCenterY": 38 } ], "currentCostumeIndex": 0, @@ -5576,11 +6226,12 @@ }, { "name": "Frank", - "md5": "19a09a005d7199d2e681e53a761bfe11.svg", + "md5": "8ebcc9a619ae0455df9da42d39f59c1d.svg", "type": "sprite", "tags": [ "fantasy", "spooky", + "people", "halloween", "frankenstein", "monster", @@ -5595,10 +6246,10 @@ "objName": "Frank", "sounds": [ { - "soundName": "pop", + "soundName": "wolf howl", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, + "md5": "5e36d74bb16aa5085b901362788b0fbf.wav", + "sampleCount": 43008, "rate": 11025, "format": "" } @@ -5607,33 +6258,33 @@ { "costumeName": "frank-a", "baseLayerID": -1, - "baseLayerMD5": "19a09a005d7199d2e681e53a761bfe11.svg", + "baseLayerMD5": "8ebcc9a619ae0455df9da42d39f59c1d.svg", "bitmapResolution": 1, - "rotationCenterX": 154, - "rotationCenterY": 107 + "rotationCenterX": 97, + "rotationCenterY": 63 }, { "costumeName": "frank-b", "baseLayerID": -1, - "baseLayerMD5": "a03102442ee197a39083fba24e852792.svg", + "baseLayerMD5": "e605b9d87181c52825ae21597c9c1e07.svg", "bitmapResolution": 1, - "rotationCenterX": 154, - "rotationCenterY": 107 + "rotationCenterX": 105, + "rotationCenterY": 63 }, { "costumeName": "frank-c", "baseLayerID": -1, - "baseLayerMD5": "b456d8dc4b7895d61c90dbc909d8a4c6.svg", + "baseLayerMD5": "a53378250f07b6dad161516a5317277e.svg", "bitmapResolution": 1, - "rotationCenterX": 154, - "rotationCenterY": 107 + "rotationCenterX": 159, + "rotationCenterY": 59 }, { "costumeName": "frank-d", "baseLayerID": -1, - "baseLayerMD5": "8743c01814fed4190a582ba1520e8f6a.svg", + "baseLayerMD5": "44b4ba838b60f14d6e7ebddf5df4d4c4.svg", "bitmapResolution": 1, - "rotationCenterX": 154, + "rotationCenterX": 106, "rotationCenterY": 107 } ], @@ -5698,6 +6349,71 @@ "spriteInfo": {} } }, + { + "name": "Frog 2 ", + "md5": "07160d455202e95f5b4b5c842eff6788.svg", + "type": "sprite", + "tags": [ + "animals", + "insect", + "bug", + "wetland", + "owen davey" + ], + "info": [ + 0, + 3, + 1 + ], + "json": { + "objName": "Frog 2 ", + "sounds": [ + { + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + } + ], + "costumes": [ + { + "costumeName": "Frog 2-a", + "baseLayerID": -1, + "baseLayerMD5": "07160d455202e95f5b4b5c842eff6788.svg", + "bitmapResolution": 1, + "rotationCenterX": 101, + "rotationCenterY": 60 + }, + { + "costumeName": "Frog 2-b", + "baseLayerID": -1, + "baseLayerMD5": "9663ddb4d100eb4e18702ee13eca835e.svg", + "bitmapResolution": 1, + "rotationCenterX": 101, + "rotationCenterY": 58 + }, + { + "costumeName": "Frog 2-c", + "baseLayerID": -1, + "baseLayerMD5": "5c2ec9de8d1695cd4b59beea45046560.svg", + "bitmapResolution": 1, + "rotationCenterX": 96, + "rotationCenterY": 87 + } + ], + "currentCostumeIndex": 0, + "scratchX": -19, + "scratchY": -6, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, { "name": "Fruit Platter", "md5": "6c3252378da3334f63eebddbed3fae91.png", @@ -5760,10 +6476,10 @@ "objName": "Fruit Salad", "sounds": [ { - "soundName": "pop", + "soundName": "Chomp", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, + "md5": "0b1e3033140d094563248e61de4039e5.wav", + "sampleCount": 2912, "rate": 11025, "format": "" } @@ -5791,7 +6507,7 @@ }, { "name": "Ghost", - "md5": "23c317a22dec95421f2532090cc8de48.svg", + "md5": "fca9fdf3ec9aafcfbd915105fecb6ce6.svg", "type": "sprite", "tags": [ "fantasy", @@ -5810,10 +6526,10 @@ "objName": "Ghost", "sounds": [ { - "soundName": "pop", + "soundName": "space ripple", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, + "md5": "ff8b8c3bf841a11fd5fe3afaa92be1b5.wav", + "sampleCount": 41149, "rate": 11025, "format": "" } @@ -5822,34 +6538,34 @@ { "costumeName": "ghost-a", "baseLayerID": -1, - "baseLayerMD5": "23c317a22dec95421f2532090cc8de48.svg", + "baseLayerMD5": "fca9fdf3ec9aafcfbd915105fecb6ce6.svg", "bitmapResolution": 1, - "rotationCenterX": 98, - "rotationCenterY": 73 + "rotationCenterX": 37, + "rotationCenterY": 68 }, { "costumeName": "ghost-b", "baseLayerID": -1, - "baseLayerMD5": "e86a742d091370950f3353e1e430cdb2.svg", + "baseLayerMD5": "9d6e168d234d023476324cb72430e645.svg", "bitmapResolution": 1, - "rotationCenterX": 98, - "rotationCenterY": 73 + "rotationCenterX": 41, + "rotationCenterY": 68 }, { "costumeName": "ghost-c", "baseLayerID": -1, - "baseLayerMD5": "24a8b71b780bc665c3e6d37c11221137.svg", + "baseLayerMD5": "8296c8aa0782b53c5188dabde5fce10f.svg", "bitmapResolution": 1, - "rotationCenterX": 98, - "rotationCenterY": 73 + "rotationCenterX": 61, + "rotationCenterY": 72 }, { "costumeName": "ghost-d", "baseLayerID": -1, - "baseLayerMD5": "221b794cad3d045008299e3c8440170c.svg", + "baseLayerMD5": "2a3c6eb1faa6b2e4403c64945d59e122.svg", "bitmapResolution": 1, - "rotationCenterX": 98, - "rotationCenterY": 73 + "rotationCenterX": 78, + "rotationCenterY": 69 } ], "currentCostumeIndex": 0, @@ -5997,7 +6713,7 @@ ], "info": [ 0, - 4, + 3, 1 ], "json": { @@ -6036,19 +6752,11 @@ "bitmapResolution": 1, "rotationCenterX": 71, "rotationCenterY": 107 - }, - { - "costumeName": "Giga walk4", - "baseLayerID": -1, - "baseLayerMD5": "22e4ae40919cf0fe6b4d7649d14a6e71.svg", - "bitmapResolution": 1, - "rotationCenterX": 73, - "rotationCenterY": 110 } ], "currentCostumeIndex": 0, - "scratchX": -95, - "scratchY": 0, + "scratchX": 160, + "scratchY": -114, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -6059,7 +6767,7 @@ }, { "name": "Giraffe", - "md5": "497baa643718a865b0621c1607696800.svg", + "md5": "2d3955997d2b3aa8dad9389c61de384f.svg", "type": "sprite", "tags": [ "animals", @@ -6075,10 +6783,10 @@ "objName": "Giraffe", "sounds": [ { - "soundName": "pop", + "soundName": "horse gallop", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, + "md5": "058a34b5fb8b57178b5322d994b6b8c8.wav", + "sampleCount": 38336, "rate": 11025, "format": "" } @@ -6087,26 +6795,26 @@ { "costumeName": "giraffe-a", "baseLayerID": -1, - "baseLayerMD5": "497baa643718a865b0621c1607696800.svg", + "baseLayerMD5": "2d3955997d2b3aa8dad9389c61de384f.svg", "bitmapResolution": 1, - "rotationCenterX": 86, - "rotationCenterY": 131 + "rotationCenterX": 87, + "rotationCenterY": 132 }, { "costumeName": "giraffe-b", "baseLayerID": -1, - "baseLayerMD5": "0242ae2497839f449af064b40b358b76.svg", + "baseLayerMD5": "b129c6016fedc7d03fb7f0a06363af28.svg", "bitmapResolution": 1, - "rotationCenterX": 86, - "rotationCenterY": 131 + "rotationCenterX": 82, + "rotationCenterY": 132 }, { "costumeName": "giraffe-c", "baseLayerID": -1, - "baseLayerMD5": "d9e8f8b7d656d4a24c1f4f6c24c5e578.svg", + "baseLayerMD5": "1af467957df0350765ba9a10e8d85572.svg", "bitmapResolution": 1, "rotationCenterX": 86, - "rotationCenterY": 131 + "rotationCenterY": 132 } ], "currentCostumeIndex": 0, @@ -6138,7 +6846,7 @@ "objName": "Glass Water", "sounds": [ { - "soundName": "water drop", + "soundName": "Water Drop", "soundID": -1, "md5": "aa488de9e2c871e9d4faecd246ed737a.wav", "sampleCount": 8136, @@ -6181,7 +6889,8 @@ "type": "sprite", "tags": [ "fashion", - "glasses" + "glasses", + "clothing" ], "info": [ 0, @@ -6235,8 +6944,8 @@ } ], "currentCostumeIndex": 0, - "scratchX": 37, - "scratchY": 102, + "scratchX": 39, + "scratchY": 100, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -6247,7 +6956,7 @@ }, { "name": "Goalie", - "md5": "86b0610ea21fdecb99795c5e6d52768c.svg", + "md5": "dbf6a1b0402377be2cd8c75a3b4fdf53.svg", "type": "sprite", "tags": [ "sports", @@ -6264,54 +6973,54 @@ "objName": "Goalie", "sounds": [ { - "soundName": "pop", + "soundName": "cheer", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "170e05c29d50918ae0b482c2955768c0.wav", + "sampleCount": 108864, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "goalie-a", "baseLayerID": -1, - "baseLayerMD5": "86b0610ea21fdecb99795c5e6d52768c.svg", + "baseLayerMD5": "dbf6a1b0402377be2cd8c75a3b4fdf53.svg", "bitmapResolution": 1, - "rotationCenterX": 76, - "rotationCenterY": 68 + "rotationCenterX": 59, + "rotationCenterY": 63 }, { "costumeName": "goalie-b", "baseLayerID": -1, - "baseLayerMD5": "af3ef5125d187772240a1120e7eb67ac.svg", + "baseLayerMD5": "987d8c2bb6c26cad265a3aa339fe432e.svg", "bitmapResolution": 1, - "rotationCenterX": 76, - "rotationCenterY": 68 + "rotationCenterX": 59, + "rotationCenterY": 63 }, { "costumeName": "goalie-c", "baseLayerID": -1, - "baseLayerMD5": "7c9377cedae11a094d2e77bed3edb884.svg", + "baseLayerMD5": "52609ef559a29052e72efe08d1384980.svg", "bitmapResolution": 1, - "rotationCenterX": 76, - "rotationCenterY": 68 + "rotationCenterX": 62, + "rotationCenterY": 61 }, { "costumeName": "goalie-d", "baseLayerID": -1, - "baseLayerMD5": "bd628034d356d30b0e9b563447471290.svg", + "baseLayerMD5": "a4de535b4c4a7c6a65e6736491e01497.svg", "bitmapResolution": 1, - "rotationCenterX": 76, - "rotationCenterY": 68 + "rotationCenterX": 63, + "rotationCenterY": 62 }, { "costumeName": "goalie-e", "baseLayerID": -1, - "baseLayerMD5": "b3f6c4c0be9a0f71e9486dea51e513c3.svg", + "baseLayerMD5": "ca55a23f3172e9fff061484e00493d2c.svg", "bitmapResolution": 1, - "rotationCenterX": 76, - "rotationCenterY": 68 + "rotationCenterX": 59, + "rotationCenterY": 69 } ], "currentCostumeIndex": 0, @@ -6343,11 +7052,11 @@ "objName": "Goblin", "sounds": [ { - "soundName": "pop", + "soundName": "fairydust", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, + "md5": "b92de59d992a655c1b542223a784cda6.wav", + "sampleCount": 11247, + "rate": 22050, "format": "" } ], @@ -6460,6 +7169,95 @@ "spriteInfo": {} } }, + { + "name": "Grasshopper", + "md5": "fcddf46c04479b07b2944ee36d0b7635.svg", + "type": "sprite", + "tags": [ + "animals", + "insect", + "bug", + "wetland", + "owen davey" + ], + "info": [ + 0, + 6, + 1 + ], + "json": { + "objName": "Grasshopper", + "sounds": [ + { + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + } + ], + "costumes": [ + { + "costumeName": "Grasshopper-a", + "baseLayerID": -1, + "baseLayerMD5": "fcddf46c04479b07b2944ee36d0b7635.svg", + "bitmapResolution": 1, + "rotationCenterX": 103, + "rotationCenterY": 43 + }, + { + "costumeName": "Grasshopper-b", + "baseLayerID": -1, + "baseLayerMD5": "40b48bcb95f06a2281ba1cad60c8c504.svg", + "bitmapResolution": 1, + "rotationCenterX": 114, + "rotationCenterY": 118 + }, + { + "costumeName": "Grasshopper-c", + "baseLayerID": -1, + "baseLayerMD5": "fa9a02a4a6df86612365dc65c065d2cb.svg", + "bitmapResolution": 1, + "rotationCenterX": 34, + "rotationCenterY": 170 + }, + { + "costumeName": "Grasshopper-d", + "baseLayerID": -1, + "baseLayerMD5": "f80ba703a7fb17a870ec0818add2f3b8.svg", + "bitmapResolution": 1, + "rotationCenterX": 70, + "rotationCenterY": 100 + }, + { + "costumeName": "Grasshopper-e", + "baseLayerID": -1, + "baseLayerMD5": "6519eeaa65eac086198d6901cf0a5b9e.svg", + "bitmapResolution": 1, + "rotationCenterX": 56, + "rotationCenterY": 86 + }, + { + "costumeName": "Grasshopper-f", + "baseLayerID": -1, + "baseLayerMD5": "8d865e6bbea03cda509d766085b883f4.svg", + "bitmapResolution": 1, + "rotationCenterX": 22, + "rotationCenterY": 54 + } + ], + "currentCostumeIndex": 0, + "scratchX": -7, + "scratchY": -115, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, { "name": "Green Flag", "md5": "173e20ac537d2c278ed621be3db3fc87.svg", @@ -6507,7 +7305,7 @@ }, { "name": "Griffin", - "md5": "d2ddc25b224ad72240f92e632afc7c69.svg", + "md5": "1ceea469ec409d0a3a13bd04ea1e756c.svg", "type": "sprite", "tags": [ "fantasy", @@ -6517,37 +7315,53 @@ ], "info": [ 0, - 2, + 4, 1 ], "json": { "objName": "Griffin", "sounds": [ { - "soundName": "pop", + "soundName": "Magic Spell", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1cb60ecdb1075c8769cb346d5c2a22c7.wav", + "sampleCount": 43077, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { - "costumeName": "griffin-a", + "costumeName": "Griffin-a", "baseLayerID": -1, - "baseLayerMD5": "d2ddc25b224ad72240f92e632afc7c69.svg", + "baseLayerMD5": "1ceea469ec409d0a3a13bd04ea1e756c.svg", "bitmapResolution": 1, - "rotationCenterX": 150, - "rotationCenterY": 110 + "rotationCenterX": 127, + "rotationCenterY": 109 }, { - "costumeName": "griffin-fly-b", + "costumeName": "Griffin-b", "baseLayerID": -1, - "baseLayerMD5": "ff0795d15b6f3990345f72bc483a3353.svg", + "baseLayerMD5": "1dd10e8e53c048ad6f95c82501801f48.svg", "bitmapResolution": 1, - "rotationCenterX": 150, - "rotationCenterY": 110 + "rotationCenterX": 127, + "rotationCenterY": 76 + }, + { + "costumeName": "Griffin-c", + "baseLayerID": -1, + "baseLayerMD5": "e4d7c3c18463f15083683ce0826adcee.svg", + "bitmapResolution": 1, + "rotationCenterX": 127, + "rotationCenterY": 109 + }, + { + "costumeName": "Griffin-d", + "baseLayerID": -1, + "baseLayerMD5": "54504b1d8dedf816b0bd599e768f191d.svg", + "bitmapResolution": 1, + "rotationCenterX": 144, + "rotationCenterY": 69 } ], "currentCostumeIndex": 0, @@ -6561,62 +7375,6 @@ "spriteInfo": {} } }, - { - "name": "Griffin-flying", - "md5": "03d75e0c7c34e8618545a5f4913db4ea.svg", - "type": "sprite", - "tags": [ - "fantasy", - "animals", - "ipzy", - "flying" - ], - "info": [ - 0, - 2, - 1 - ], - "json": { - "objName": "Griffin-flying", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "griffin-fly-a", - "baseLayerID": -1, - "baseLayerMD5": "03d75e0c7c34e8618545a5f4913db4ea.svg", - "bitmapResolution": 1, - "rotationCenterX": 150, - "rotationCenterY": 110 - }, - { - "costumeName": "griffin-fly-b", - "baseLayerID": -1, - "baseLayerMD5": "ff0795d15b6f3990345f72bc483a3353.svg", - "bitmapResolution": 1, - "rotationCenterX": 150, - "rotationCenterY": 110 - } - ], - "currentCostumeIndex": 0, - "scratchX": -13, - "scratchY": 1, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, { "name": "Guitar", "md5": "cb8c2a5e69da7538e1dd73cb7ff4a666.svg", @@ -6634,7 +7392,7 @@ "objName": "Guitar", "sounds": [ { - "soundName": "C guitar", + "soundName": "C Guitar", "soundID": -1, "md5": "22baa07795a9a524614075cdea543793.wav", "sampleCount": 44864, @@ -6642,7 +7400,7 @@ "format": "" }, { - "soundName": "D guitar", + "soundName": "D Guitar", "soundID": -1, "md5": "2dbcfae6a55738f94bbb40aa5fcbf7ce.wav", "sampleCount": 41120, @@ -6650,7 +7408,7 @@ "format": "" }, { - "soundName": "E guitar", + "soundName": "E Guitar", "soundID": -1, "md5": "4b5d1da83e59bf35578324573c991666.wav", "sampleCount": 38400, @@ -6658,7 +7416,7 @@ "format": "" }, { - "soundName": "F guitar", + "soundName": "F Guitar", "soundID": -1, "md5": "b51d086aeb1921ec405561df52ecbc50.wav", "sampleCount": 36416, @@ -6666,7 +7424,7 @@ "format": "" }, { - "soundName": "G guitar", + "soundName": "G Guitar", "soundID": -1, "md5": "98a835713ecea2f3ef9f4f442d52ad20.wav", "sampleCount": 33600, @@ -6674,7 +7432,7 @@ "format": "" }, { - "soundName": "A guitar", + "soundName": "A Guitar", "soundID": -1, "md5": "ee753e87d212d4b2fb650ca660f1e839.wav", "sampleCount": 31872, @@ -6682,7 +7440,7 @@ "format": "" }, { - "soundName": "B guitar", + "soundName": "B Guitar", "soundID": -1, "md5": "2ae2d67de62df8ca54d638b4ad2466c3.wav", "sampleCount": 29504, @@ -6728,117 +7486,7 @@ } }, { - "name": "Guitar-electric2", - "md5": "1fc433b89038f9e16092c9f4d7514cca.svg", - "type": "sprite", - "tags": [ - "music", - "andrew rae" - ], - "info": [ - 0, - 2, - 8 - ], - "json": { - "objName": "Guitar-electric2", - "sounds": [ - { - "soundName": "C elec guitar", - "soundID": -1, - "md5": "0d340de02e14bebaf8dfa0e43eb3f1f9.wav", - "sampleCount": 44100, - "rate": 22050, - "format": "" - }, - { - "soundName": "D elec guitar", - "soundID": -1, - "md5": "1b5de9866801eb2f9d4f57c7c3b473f5.wav", - "sampleCount": 44100, - "rate": 22050, - "format": "" - }, - { - "soundName": "E elec guitar", - "soundID": -1, - "md5": "2e6a6ae3e0f72bf78c74def8130f459a.wav", - "sampleCount": 44100, - "rate": 22050, - "format": "" - }, - { - "soundName": "F elec guitar", - "soundID": -1, - "md5": "5eb00f15f21f734986aa45156d44478d.wav", - "sampleCount": 44100, - "rate": 22050, - "format": "" - }, - { - "soundName": "G elec guitar", - "soundID": -1, - "md5": "cd0d0e7dad415b2ffa2ba7a61860eaf8.wav", - "sampleCount": 44100, - "rate": 22050, - "format": "" - }, - { - "soundName": "A elec guitar", - "soundID": -1, - "md5": "fa5f7fea601e9368dd68449d9a54c995.wav", - "sampleCount": 44100, - "rate": 22050, - "format": "" - }, - { - "soundName": "B elec guitar", - "soundID": -1, - "md5": "81f142d0b00189703d7fe9b1f13f6f87.wav", - "sampleCount": 44100, - "rate": 22050, - "format": "" - }, - { - "soundName": "C2 elec guitar", - "soundID": -1, - "md5": "3a8ed3129f22cba5b0810bc030d16b5f.wav", - "sampleCount": 44100, - "rate": 22050, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "guitar-electric2-a", - "baseLayerID": -1, - "baseLayerMD5": "1fc433b89038f9e16092c9f4d7514cca.svg", - "bitmapResolution": 1, - "rotationCenterX": 38, - "rotationCenterY": 94 - }, - { - "costumeName": "guitar-electric2-b", - "baseLayerID": -1, - "baseLayerMD5": "7b843dbc93d4b2ea31fa67cca3d5077c.svg", - "bitmapResolution": 1, - "rotationCenterX": 38, - "rotationCenterY": 94 - } - ], - "currentCostumeIndex": 0, - "scratchX": -99, - "scratchY": 17, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, - { - "name": "Gutar-electric1", + "name": "Guitar-electric1", "md5": "b2b469b9d11fd23bdd671eab94dc58ff.svg", "type": "sprite", "tags": [ @@ -6851,10 +7499,10 @@ 8 ], "json": { - "objName": "Gutar-electric1", + "objName": "Guitar-electric1", "sounds": [ { - "soundName": "C elec guitar", + "soundName": "C Elec Guitar", "soundID": -1, "md5": "0d340de02e14bebaf8dfa0e43eb3f1f9.wav", "sampleCount": 44100, @@ -6862,7 +7510,7 @@ "format": "" }, { - "soundName": "D elec guitar", + "soundName": "D Elec Guitar", "soundID": -1, "md5": "1b5de9866801eb2f9d4f57c7c3b473f5.wav", "sampleCount": 44100, @@ -6870,7 +7518,7 @@ "format": "" }, { - "soundName": "E elec guitar", + "soundName": "E Elec Guitar", "soundID": -1, "md5": "2e6a6ae3e0f72bf78c74def8130f459a.wav", "sampleCount": 44100, @@ -6878,7 +7526,7 @@ "format": "" }, { - "soundName": "F elec guitar", + "soundName": "F Elec Guitar", "soundID": -1, "md5": "5eb00f15f21f734986aa45156d44478d.wav", "sampleCount": 44100, @@ -6886,7 +7534,7 @@ "format": "" }, { - "soundName": "G elec guitar", + "soundName": "G Elec Guitar", "soundID": -1, "md5": "cd0d0e7dad415b2ffa2ba7a61860eaf8.wav", "sampleCount": 44100, @@ -6894,7 +7542,7 @@ "format": "" }, { - "soundName": "A elec guitar", + "soundName": "A Elec Guitar", "soundID": -1, "md5": "fa5f7fea601e9368dd68449d9a54c995.wav", "sampleCount": 44100, @@ -6902,7 +7550,7 @@ "format": "" }, { - "soundName": "B elec guitar", + "soundName": "B Elec Guitar", "soundID": -1, "md5": "81f142d0b00189703d7fe9b1f13f6f87.wav", "sampleCount": 44100, @@ -6910,7 +7558,7 @@ "format": "" }, { - "soundName": "C2 elec guitar", + "soundName": "C2 Elec Guitar", "soundID": -1, "md5": "3a8ed3129f22cba5b0810bc030d16b5f.wav", "sampleCount": 44100, @@ -6947,6 +7595,116 @@ "spriteInfo": {} } }, + { + "name": "Guitar-electric2", + "md5": "1fc433b89038f9e16092c9f4d7514cca.svg", + "type": "sprite", + "tags": [ + "music", + "andrew rae" + ], + "info": [ + 0, + 2, + 8 + ], + "json": { + "objName": "Guitar-electric2", + "sounds": [ + { + "soundName": "C Elec Guitar", + "soundID": -1, + "md5": "0d340de02e14bebaf8dfa0e43eb3f1f9.wav", + "sampleCount": 44100, + "rate": 22050, + "format": "" + }, + { + "soundName": "D Elec Guitar", + "soundID": -1, + "md5": "1b5de9866801eb2f9d4f57c7c3b473f5.wav", + "sampleCount": 44100, + "rate": 22050, + "format": "" + }, + { + "soundName": "E Elec Guitar", + "soundID": -1, + "md5": "2e6a6ae3e0f72bf78c74def8130f459a.wav", + "sampleCount": 44100, + "rate": 22050, + "format": "" + }, + { + "soundName": "F Elec Guitar", + "soundID": -1, + "md5": "5eb00f15f21f734986aa45156d44478d.wav", + "sampleCount": 44100, + "rate": 22050, + "format": "" + }, + { + "soundName": "G Elec Guitar", + "soundID": -1, + "md5": "cd0d0e7dad415b2ffa2ba7a61860eaf8.wav", + "sampleCount": 44100, + "rate": 22050, + "format": "" + }, + { + "soundName": "A Elec Guitar", + "soundID": -1, + "md5": "fa5f7fea601e9368dd68449d9a54c995.wav", + "sampleCount": 44100, + "rate": 22050, + "format": "" + }, + { + "soundName": "B Elec Guitar", + "soundID": -1, + "md5": "81f142d0b00189703d7fe9b1f13f6f87.wav", + "sampleCount": 44100, + "rate": 22050, + "format": "" + }, + { + "soundName": "C2 Elec Guitar", + "soundID": -1, + "md5": "3a8ed3129f22cba5b0810bc030d16b5f.wav", + "sampleCount": 44100, + "rate": 22050, + "format": "" + } + ], + "costumes": [ + { + "costumeName": "guitar-electric2-a", + "baseLayerID": -1, + "baseLayerMD5": "1fc433b89038f9e16092c9f4d7514cca.svg", + "bitmapResolution": 1, + "rotationCenterX": 38, + "rotationCenterY": 94 + }, + { + "costumeName": "guitar-electric2-b", + "baseLayerID": -1, + "baseLayerMD5": "7b843dbc93d4b2ea31fa67cca3d5077c.svg", + "bitmapResolution": 1, + "rotationCenterX": 38, + "rotationCenterY": 94 + } + ], + "currentCostumeIndex": 0, + "scratchX": -99, + "scratchY": 17, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, { "name": "Hannah", "md5": "b983d99560313e38b4b3cd36cbd5f0d1.png", @@ -7011,7 +7769,7 @@ }, { "name": "Hare", - "md5": "75c2e6085d0eae076ff357b56d5ed5cd.svg", + "md5": "8209c43050785e3c4276f84abb52c245.svg", "type": "sprite", "tags": [ "animals", @@ -7045,26 +7803,26 @@ { "costumeName": "hare-a", "baseLayerID": -1, - "baseLayerMD5": "75c2e6085d0eae076ff357b56d5ed5cd.svg", + "baseLayerMD5": "8209c43050785e3c4276f84abb52c245.svg", "bitmapResolution": 1, - "rotationCenterX": 57, - "rotationCenterY": 49 + "rotationCenterX": 29, + "rotationCenterY": 50 }, { "costumeName": "hare-b", "baseLayerID": -1, - "baseLayerMD5": "fb56d1bef942196c710612906142451b.svg", + "baseLayerMD5": "62796e48dc8e540398e0575d79f562fe.svg", "bitmapResolution": 1, "rotationCenterX": 57, - "rotationCenterY": 49 + "rotationCenterY": 35 }, { "costumeName": "hare-c", "baseLayerID": -1, - "baseLayerMD5": "8de391ae70e54656f777c0ed53a91e1a.svg", + "baseLayerMD5": "4bc5c29283dfff4b9c876cc23540a4b5.svg", "bitmapResolution": 1, - "rotationCenterX": 57, - "rotationCenterY": 49 + "rotationCenterX": 40, + "rotationCenterY": 40 } ], "currentCostumeIndex": 0, @@ -7080,11 +7838,12 @@ }, { "name": "Harper", - "md5": "919eb332bf73df227ec5f214be952a21.svg", + "md5": "0d5979db40b0a4dcaa601fb7679ef71c.svg", "type": "sprite", "tags": [ "people", - "fashion" + "fashion", + "clothing" ], "info": [ 0, @@ -7107,19 +7866,19 @@ { "costumeName": "harper-a", "baseLayerID": -1, - "baseLayerMD5": "919eb332bf73df227ec5f214be952a21.svg", - "bitmapResolution": 1, - "rotationCenterX": 54, - "rotationCenterY": 135 - }, - { - "costumeName": "harper-b", - "baseLayerID": -1, "baseLayerMD5": "0d5979db40b0a4dcaa601fb7679ef71c.svg", "bitmapResolution": 1, "rotationCenterX": 54, "rotationCenterY": 143 }, + { + "costumeName": "harper-b", + "baseLayerID": -1, + "baseLayerMD5": "c435a1fcdf5f60a29f620fa800e391e0.svg", + "bitmapResolution": 1, + "rotationCenterX": 54, + "rotationCenterY": 134 + }, { "costumeName": "harper-c", "baseLayerID": -1, @@ -7140,287 +7899,14 @@ "spriteInfo": {} } }, - { - "name": "Hat", - "md5": "b3beb1f52d371428d70b65a0c4c5c001.svg", - "type": "sprite", - "tags": [ - "Fashion" - ], - "info": [ - 0, - 1, - 1 - ], - "json": { - "objName": "Hat", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "Hat", - "baseLayerID": -1, - "baseLayerMD5": "b3beb1f52d371428d70b65a0c4c5c001.svg", - "bitmapResolution": 1, - "rotationCenterX": 52, - "rotationCenterY": 60 - } - ], - "currentCostumeIndex": 0, - "scratchX": -15, - "scratchY": 7, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, - { - "name": "Hat Beanie", - "md5": "3271da33e4108ed08a303c2244739fbf.svg", - "type": "sprite", - "tags": [ - "fashion" - ], - "info": [ - 0, - 1, - 1 - ], - "json": { - "objName": "Hat Beanie", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "hat beanie", - "baseLayerID": -1, - "baseLayerMD5": "3271da33e4108ed08a303c2244739fbf.svg", - "bitmapResolution": 1, - "rotationCenterX": 28, - "rotationCenterY": 19 - } - ], - "currentCostumeIndex": 0, - "scratchX": 40, - "scratchY": 22, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, - { - "name": "Hat Party1", - "md5": "70a7f535d8857cf9175492415361c361.svg", - "type": "sprite", - "tags": [ - "fashion", - "holiday" - ], - "info": [ - 0, - 1, - 1 - ], - "json": { - "objName": "Hat Party1", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "partyhat1", - "baseLayerID": -1, - "baseLayerMD5": "70a7f535d8857cf9175492415361c361.svg", - "bitmapResolution": 1, - "rotationCenterX": 75, - "rotationCenterY": 75 - } - ], - "currentCostumeIndex": 0, - "scratchX": 27, - "scratchY": 26, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, - { - "name": "Hat Party2", - "md5": "9b7a84fe3e50621752917e4e484a1e2f.svg", - "type": "sprite", - "tags": [ - "fashion", - "holiday" - ], - "info": [ - 0, - 1, - 1 - ], - "json": { - "objName": "Hat Party2", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "hat party2-a", - "baseLayerID": -1, - "baseLayerMD5": "9b7a84fe3e50621752917e4e484a1e2f.svg", - "bitmapResolution": 1, - "rotationCenterX": 75, - "rotationCenterY": 75 - } - ], - "currentCostumeIndex": 0, - "scratchX": -80, - "scratchY": -31, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, - { - "name": "Hat Winter", - "md5": "6c2ee1b97f6ec2b3457a25a3975a2009.svg", - "type": "sprite", - "tags": [ - "fashion", - "winter" - ], - "info": [ - 0, - 1, - 1 - ], - "json": { - "objName": "Hat Winter", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "hat winter", - "baseLayerID": -1, - "baseLayerMD5": "6c2ee1b97f6ec2b3457a25a3975a2009.svg", - "bitmapResolution": 1, - "rotationCenterX": 26, - "rotationCenterY": 101 - } - ], - "currentCostumeIndex": 0, - "scratchX": -30, - "scratchY": -42, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, - { - "name": "Hat Wizard", - "md5": "581571e8c8f5adeb01565e12b1b77b58.svg", - "type": "sprite", - "tags": [ - "fashion", - "fantasy" - ], - "info": [ - 0, - 1, - 1 - ], - "json": { - "objName": "Hat Wizard", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "hat wizard", - "baseLayerID": -1, - "baseLayerMD5": "581571e8c8f5adeb01565e12b1b77b58.svg", - "bitmapResolution": 1, - "rotationCenterX": 76, - "rotationCenterY": 69 - } - ], - "currentCostumeIndex": 0, - "scratchX": -7, - "scratchY": 6, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, { "name": "Hat1 ", - "md5": "874462b3e079308d775629c235940d8f.svg", + "md5": "52540f9dba4537f79f1ad3067d44e3f6.svg", "type": "sprite", "tags": [ "fashion", - "hat" + "hat", + "clothing" ], "info": [ 0, @@ -7443,34 +7929,34 @@ { "costumeName": "hat-a", "baseLayerID": -1, - "baseLayerMD5": "874462b3e079308d775629c235940d8f.svg", - "bitmapResolution": 1, - "rotationCenterX": 39, - "rotationCenterY": 22 - }, - { - "costumeName": "hat-b", - "baseLayerID": -1, - "baseLayerMD5": "a8ef874bd7542fec62785eac80019417.svg", - "bitmapResolution": 1, - "rotationCenterX": 45, - "rotationCenterY": 30 - }, - { - "costumeName": "hat-c", - "baseLayerID": -1, "baseLayerMD5": "52540f9dba4537f79f1ad3067d44e3f6.svg", "bitmapResolution": 1, "rotationCenterX": 37, "rotationCenterY": 30 }, { - "costumeName": "hat-d", + "costumeName": "hat-b", + "baseLayerID": -1, + "baseLayerMD5": "874462b3e079308d775629c235940d8f.svg", + "bitmapResolution": 1, + "rotationCenterX": 39, + "rotationCenterY": 22 + }, + { + "costumeName": "hat-c", "baseLayerID": -1, "baseLayerMD5": "b40accecd4bd57fed8bdb96ee15e5c05.svg", "bitmapResolution": 1, "rotationCenterX": 40, "rotationCenterY": 27 + }, + { + "costumeName": "hat-d", + "baseLayerID": -1, + "baseLayerMD5": "a8ef874bd7542fec62785eac80019417.svg", + "bitmapResolution": 1, + "rotationCenterX": 45, + "rotationCenterY": 30 } ], "currentCostumeIndex": 0, @@ -7486,7 +7972,7 @@ }, { "name": "Hatchling", - "md5": "fb9de507b6d95e96cb0df68e401dfd8f.svg", + "md5": "25e4da6bcd209c60690729f5c7016b83.svg", "type": "sprite", "tags": [ "animals", @@ -7503,89 +7989,43 @@ "objName": "Hatchling", "sounds": [ { - "soundName": "pop", + "soundName": "Chirp", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "3b8236bbb288019d93ae38362e865972.wav", + "sampleCount": 5301, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "hatchling-a", "baseLayerID": -1, - "baseLayerMD5": "fb9de507b6d95e96cb0df68e401dfd8f.svg", + "baseLayerMD5": "25e4da6bcd209c60690729f5c7016b83.svg", "bitmapResolution": 1, - "rotationCenterX": 14, + "rotationCenterX": 15, "rotationCenterY": 23 }, { "costumeName": "hatchling-b", "baseLayerID": -1, - "baseLayerMD5": "e504303f791a65274d740a20acbac606.svg", + "baseLayerMD5": "b0f8451e7c52cf2b94b94c4a2051398b.svg", "bitmapResolution": 1, - "rotationCenterX": 15, - "rotationCenterY": 35 + "rotationCenterX": 16, + "rotationCenterY": 38 }, { "costumeName": "hatchling-c", "baseLayerID": -1, - "baseLayerMD5": "6f9a243d84fbe7ed9a49e8ef9fb7d83c.svg", + "baseLayerMD5": "5f70ae46957bf425121ac532dc6a2f34.svg", "bitmapResolution": 1, - "rotationCenterX": 31, - "rotationCenterY": 36 + "rotationCenterX": 38, + "rotationCenterY": 72 } ], "currentCostumeIndex": 0, - "scratchX": -174, - "scratchY": -85, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, - { - "name": "Headband", - "md5": "237f77cb3e41c70f714c6d2aa4d38ea1.svg", - "type": "sprite", - "tags": [ - "fashion", - "antennae" - ], - "info": [ - 0, - 1, - 1 - ], - "json": { - "objName": "Headband", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "headband", - "baseLayerID": -1, - "baseLayerMD5": "237f77cb3e41c70f714c6d2aa4d38ea1.svg", - "bitmapResolution": 1, - "rotationCenterX": 53, - "rotationCenterY": 43 - } - ], - "currentCostumeIndex": 0, - "scratchX": 86, - "scratchY": -38, + "scratchX": -156, + "scratchY": -76, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -7653,10 +8093,11 @@ }, { "name": "Heart Candy", - "md5": "d448acd247f10f32bef7823cd433f928.svg", + "md5": "1bc73fc472df673a1089d965b537f5ac.svg", "type": "sprite", "tags": [ - "food.sweethearts" + "food", + "sweethearts" ], "info": [ 0, @@ -7676,37 +8117,37 @@ } ], "costumes": [ - { - "costumeName": "heart love it", - "baseLayerID": -1, - "baseLayerMD5": "d448acd247f10f32bef7823cd433f928.svg", - "bitmapResolution": 1, - "rotationCenterX": 58, - "rotationCenterY": 61 - }, { "costumeName": "heart code", "baseLayerID": -1, - "baseLayerMD5": "497c5df9e02467202ff93096dccaf91f.svg", + "baseLayerMD5": "1bc73fc472df673a1089d965b537f5ac.svg", "bitmapResolution": 1, - "rotationCenterX": 58, - "rotationCenterY": 61 + "rotationCenterX": 73, + "rotationCenterY": 72 + }, + { + "costumeName": "heart love", + "baseLayerID": -1, + "baseLayerMD5": "a9d25b961eef81521c8ed38beb1456a0.svg", + "bitmapResolution": 1, + "rotationCenterX": 73, + "rotationCenterY": 72 }, { "costumeName": "heart sweet", "baseLayerID": -1, - "baseLayerMD5": "a39d78d33b051e8b12a4b2a10d77b249.svg", + "baseLayerMD5": "9a9a8efa369379ee8538632c43a6a3d1.svg", "bitmapResolution": 1, - "rotationCenterX": 58, - "rotationCenterY": 61 + "rotationCenterX": 73, + "rotationCenterY": 72 }, { "costumeName": "heart smile", "baseLayerID": -1, - "baseLayerMD5": "74a8f75d139d330b715787edbbacd83d.svg", + "baseLayerMD5": "66b29ca063bc74d2bacfc0a06bc7b6fc.svg", "bitmapResolution": 1, - "rotationCenterX": 58, - "rotationCenterY": 61 + "rotationCenterX": 73, + "rotationCenterY": 72 } ], "currentCostumeIndex": 0, @@ -7847,7 +8288,7 @@ }, { "name": "Hen", - "md5": "795ed34983a70c8690fe6c8f4a47a935.svg", + "md5": "eb0a9890a92ef616ef8740328eeabb79.svg", "type": "sprite", "tags": [ "animals", @@ -7864,10 +8305,10 @@ "objName": "Hen", "sounds": [ { - "soundName": "pop", + "soundName": "bird", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, + "md5": "18bd4b634a3f992a16b30344c7d810e0.wav", + "sampleCount": 3840, "rate": 11025, "format": "" } @@ -7876,39 +8317,39 @@ { "costumeName": "hen-a", "baseLayerID": -1, - "baseLayerMD5": "795ed34983a70c8690fe6c8f4a47a935.svg", + "baseLayerMD5": "eb0a9890a92ef616ef8740328eeabb79.svg", "bitmapResolution": 1, - "rotationCenterX": 45, - "rotationCenterY": 43 + "rotationCenterX": 60, + "rotationCenterY": 53 }, { "costumeName": "hen-b", "baseLayerID": -1, - "baseLayerMD5": "26528a6f0318a2d2294ef7071409be12.svg", + "baseLayerMD5": "12aabf8636096f223e9297d2fd8177a4.svg", "bitmapResolution": 1, - "rotationCenterX": 45, - "rotationCenterY": 43 + "rotationCenterX": 63, + "rotationCenterY": 50 }, { "costumeName": "hen-c", "baseLayerID": -1, - "baseLayerMD5": "7492692df7776c905fdbd4382696afee.svg", + "baseLayerMD5": "af2c57d1f12272335629635e669df487.svg", "bitmapResolution": 1, - "rotationCenterX": 35, - "rotationCenterY": 28 + "rotationCenterX": 56, + "rotationCenterY": 53 }, { "costumeName": "hen-d", "baseLayerID": -1, - "baseLayerMD5": "4bf0cb5453273d0f75db3cd0cd45ac72.svg", + "baseLayerMD5": "84d0492c094ec01d73f808c92b4c3138.svg", "bitmapResolution": 1, - "rotationCenterX": 45, - "rotationCenterY": 43 + "rotationCenterX": 51, + "rotationCenterY": 77 } ], "currentCostumeIndex": 0, - "scratchX": -18, - "scratchY": -16, + "scratchX": 141, + "scratchY": -104, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -8184,7 +8625,7 @@ }, { "name": "Jamal", - "md5": "90f9166fe6500d0c0caad8b1964d6b74.svg", + "md5": "c7531e6c05aa488edd6ad3c369204452.svg", "type": "sprite", "tags": [ "sports", @@ -8195,7 +8636,7 @@ "info": [ 0, 4, - 1 + 2 ], "json": { "objName": "Jamal", @@ -8207,40 +8648,48 @@ "sampleCount": 258, "rate": 11025, "format": "" + }, + { + "soundName": "basketball bounce", + "soundID": -1, + "md5": "1727f65b5f22d151685b8e5917456a60.wav", + "sampleCount": 8099, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "jamal-a", "baseLayerID": -1, - "baseLayerMD5": "90f9166fe6500d0c0caad8b1964d6b74.svg", + "baseLayerMD5": "c7531e6c05aa488edd6ad3c369204452.svg", "bitmapResolution": 1, - "rotationCenterX": 82, - "rotationCenterY": 105 + "rotationCenterX": 55, + "rotationCenterY": 49 }, { "costumeName": "jamal-b", "baseLayerID": -1, - "baseLayerMD5": "c3d96ef7e99440c2fa76effce1235d3f.svg", + "baseLayerMD5": "8b8bd6ff0fe64e7056d1e70c63b9ec9f.svg", "bitmapResolution": 1, - "rotationCenterX": 82, - "rotationCenterY": 105 + "rotationCenterX": 75, + "rotationCenterY": 53 }, { "costumeName": "jamal-c", "baseLayerID": -1, - "baseLayerMD5": "1fb8b9ca79f2c0a327913bd647b53fe5.svg", + "baseLayerMD5": "c659f292333f93854be243f84b337ec5.svg", "bitmapResolution": 1, - "rotationCenterX": 82, - "rotationCenterY": 105 + "rotationCenterX": 54, + "rotationCenterY": 102 }, { "costumeName": "jamal-d", "baseLayerID": -1, - "baseLayerMD5": "4adb87e6123161fcaf02f7ac022a5757.svg", + "baseLayerMD5": "9dc8cfa22793c43aee826c76d144e09b.svg", "bitmapResolution": 1, - "rotationCenterX": 82, - "rotationCenterY": 105 + "rotationCenterX": 54, + "rotationCenterY": 95 } ], "currentCostumeIndex": 0, @@ -8309,59 +8758,6 @@ "spriteInfo": {} } }, - { - "name": "Jeans", - "md5": "4e283da8c59bcbb9803bdc0016b14c21.svg", - "type": "sprite", - "tags": [ - "fashion" - ], - "info": [ - 0, - 2, - 1 - ], - "json": { - "objName": "Jeans", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "jeans-a", - "baseLayerID": -1, - "baseLayerMD5": "4e283da8c59bcbb9803bdc0016b14c21.svg", - "bitmapResolution": 1, - "rotationCenterX": 22, - "rotationCenterY": 25 - }, - { - "costumeName": "jeans-b", - "baseLayerID": -1, - "baseLayerMD5": "01732aa03a48482093fbed3ea402c4a9.svg", - "bitmapResolution": 1, - "rotationCenterX": 22, - "rotationCenterY": 25 - } - ], - "currentCostumeIndex": 0, - "scratchX": 69, - "scratchY": 26, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, { "name": "Jellyfish", "md5": "9e6563e417350af3094c2ed02b9b0bbd.svg", @@ -8377,18 +8773,26 @@ "info": [ 0, 4, - 1 + 2 ], "json": { "objName": "Jellyfish", "sounds": [ { - "soundName": "pop", + "soundName": "bubbles", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, + "md5": "78b0be9c9c2f664158b886bc7e794095.wav", + "sampleCount": 45056, "rate": 11025, "format": "" + }, + { + "soundName": "ocean wave", + "soundID": -1, + "md5": "c904610d770398b98872a708a2f75611.wav", + "sampleCount": 99206, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -8438,7 +8842,7 @@ }, { "name": "Jordyn", - "md5": "8dd2a2abbb8e639da8576b6e72ef9e59.svg", + "md5": "8355df83cca2baa0fdf6269d1311f1ea.svg", "type": "sprite", "tags": [ "sports", @@ -8456,46 +8860,46 @@ "objName": "Jordyn", "sounds": [ { - "soundName": "pop", + "soundName": "basketball bounce", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1727f65b5f22d151685b8e5917456a60.wav", + "sampleCount": 8099, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "jordyn-a", "baseLayerID": -1, - "baseLayerMD5": "8dd2a2abbb8e639da8576b6e72ef9e59.svg", + "baseLayerMD5": "8355df83cca2baa0fdf6269d1311f1ea.svg", "bitmapResolution": 1, - "rotationCenterX": 76, - "rotationCenterY": 68 + "rotationCenterX": 51, + "rotationCenterY": 63 }, { "costumeName": "jordyn-b", "baseLayerID": -1, - "baseLayerMD5": "9665f543147961551d8dc6f612d9cc41.svg", + "baseLayerMD5": "2e2c43bbd7b18524fb132f1426ca3134.svg", "bitmapResolution": 1, - "rotationCenterX": 76, - "rotationCenterY": 68 + "rotationCenterX": 51, + "rotationCenterY": 63 }, { "costumeName": "jordyn-c", "baseLayerID": -1, - "baseLayerMD5": "ec8c2286070c77ebd9dd40c96eaae3fc.svg", + "baseLayerMD5": "5c0d24c53805e16358a2f6f6cc4a456e.svg", "bitmapResolution": 1, - "rotationCenterX": 76, - "rotationCenterY": 68 + "rotationCenterX": 68, + "rotationCenterY": 61 }, { "costumeName": "jordyn-d", "baseLayerID": -1, - "baseLayerMD5": "1f9ed7f29800f31ce2ee53196143a3c8.svg", + "baseLayerMD5": "ebe2b20b99c1e1b373fd2b02be48aab9.svg", "bitmapResolution": 1, - "rotationCenterX": 76, - "rotationCenterY": 68 + "rotationCenterX": 40, + "rotationCenterY": 61 } ], "currentCostumeIndex": 0, @@ -8537,7 +8941,7 @@ "costumes": [ { "costumeName": "jo stance", - "baseLayerID": 8, + "baseLayerID": -1, "baseLayerMD5": "6f68790ee3eb9bdccf8749305186b0dd.png", "bitmapResolution": 2, "rotationCenterX": 94, @@ -8633,7 +9037,7 @@ }, { "costumeName": "jo pop R arm", - "baseLayerID": 9, + "baseLayerID": -1, "baseLayerMD5": "aabfedff0d11243386b6b0941e0f72e9.png", "bitmapResolution": 2, "rotationCenterX": 108, @@ -8767,7 +9171,7 @@ "objName": "Keyboard", "sounds": [ { - "soundName": "C elec piano", + "soundName": "C Elec Piano", "soundID": -1, "md5": "8366ee963cc57ad24a8a35a26f722c2b.wav", "sampleCount": 44100, @@ -8775,7 +9179,7 @@ "format": "" }, { - "soundName": "D elec piano", + "soundName": "D Elec Piano", "soundID": -1, "md5": "835f136ca8d346a17b4d4baf8405be37.wav", "sampleCount": 44100, @@ -8783,7 +9187,7 @@ "format": "" }, { - "soundName": "E elec piano", + "soundName": "E Elec Piano", "soundID": -1, "md5": "ab3c198f8e36efff14f0a5bad35fa3cd.wav", "sampleCount": 44100, @@ -8791,7 +9195,7 @@ "format": "" }, { - "soundName": "F elec piano", + "soundName": "F Elec Piano", "soundID": -1, "md5": "dc5e368fc0d0dad1da609bfc3e29aa15.wav", "sampleCount": 44100, @@ -8799,7 +9203,7 @@ "format": "" }, { - "soundName": "G elec piano", + "soundName": "G Elec Piano", "soundID": -1, "md5": "39525f6545d62a95d05153f92d63301a.wav", "sampleCount": 44100, @@ -8807,7 +9211,7 @@ "format": "" }, { - "soundName": "A elec piano", + "soundName": "A Elec Piano", "soundID": -1, "md5": "0cfa8e84d6a5cd63afa31d541625a9ef.wav", "sampleCount": 44100, @@ -8815,7 +9219,7 @@ "format": "" }, { - "soundName": "B elec piano", + "soundName": "B Elec Piano", "soundID": -1, "md5": "9cc77167419f228503dd57fddaa5b2a6.wav", "sampleCount": 44100, @@ -8823,7 +9227,7 @@ "format": "" }, { - "soundName": "C2 elec piano", + "soundName": "C2 Elec Piano", "soundID": -1, "md5": "366c7edbd4dd5cca68bf62902999bd66.wav", "sampleCount": 44100, @@ -9189,7 +9593,7 @@ "md5": "c018a3eed966d5f92c69f2188dfd2aae.svg", "type": "sprite", "tags": [ - "animal", + "animals", "insect", "arthropod", "antennae", @@ -9385,7 +9789,7 @@ }, { "name": "Lion", - "md5": "701a41dec023bc03a5ad4e35988b9a86.svg", + "md5": "75e6b2178518fa354f1ebf7ab7061ceb.svg", "type": "sprite", "tags": [ "cat", @@ -9397,7 +9801,7 @@ "info": [ 0, 3, - 1 + 2 ], "json": { "objName": "Lion", @@ -9409,13 +9813,21 @@ "sampleCount": 258, "rate": 11025, "format": "" + }, + { + "soundName": "grunt", + "soundID": -1, + "md5": "caa0a1685ef7a5334413834c6c818c5a.wav", + "sampleCount": 20551, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "lion-a", "baseLayerID": -1, - "baseLayerMD5": "701a41dec023bc03a5ad4e35988b9a86.svg", + "baseLayerMD5": "75e6b2178518fa354f1ebf7ab7061ceb.svg", "bitmapResolution": 1, "rotationCenterX": 95, "rotationCenterY": 43 @@ -9423,15 +9835,15 @@ { "costumeName": "lion-b", "baseLayerID": -1, - "baseLayerMD5": "28ac215a2574eeabc2b390a14de3c152.svg", + "baseLayerMD5": "7304176fe4dc85758e6b77324a185ea2.svg", "bitmapResolution": 1, - "rotationCenterX": 95, + "rotationCenterX": 94, "rotationCenterY": 43 }, { "costumeName": "lion-c", "baseLayerID": -1, - "baseLayerMD5": "4dbe981721314eb631ef4f7fa920e1a4.svg", + "baseLayerMD5": "8411de793909216761cd19fd95418945.svg", "bitmapResolution": 1, "rotationCenterX": 95, "rotationCenterY": 43 @@ -9450,7 +9862,7 @@ }, { "name": "Llama", - "md5": "07158eb6d62e309bb60a6bc36baf2300.svg", + "md5": "f5841f36b41c4df26f9c724d913c279b.svg", "type": "sprite", "tags": [ "animals", @@ -9466,38 +9878,38 @@ "objName": "Llama", "sounds": [ { - "soundName": "pop", + "soundName": "snort", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "362d7440a57cab29914fecea621e50d4.wav", + "sampleCount": 16421, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "llama", "baseLayerID": -1, - "baseLayerMD5": "07158eb6d62e309bb60a6bc36baf2300.svg", + "baseLayerMD5": "f5841f36b41c4df26f9c724d913c279b.svg", "bitmapResolution": 1, - "rotationCenterX": 100, - "rotationCenterY": 100 + "rotationCenterX": 72, + "rotationCenterY": 95 }, { "costumeName": "llama-b", "baseLayerID": -1, - "baseLayerMD5": "2021eea71514bd2b23e96076750727ae.svg", + "baseLayerMD5": "ef3f8491668de2b751d8d7a8921c9879.svg", "bitmapResolution": 1, - "rotationCenterX": 100, - "rotationCenterY": 100 + "rotationCenterX": 92, + "rotationCenterY": 90 }, { "costumeName": "llama-c", "baseLayerID": -1, - "baseLayerMD5": "7837d7247acbc4eebb793452a35aa1f5.svg", + "baseLayerMD5": "b0af19d2c7a7c039c060f21874816164.svg", "bitmapResolution": 1, - "rotationCenterX": 100, - "rotationCenterY": 100 + "rotationCenterX": 73, + "rotationCenterY": 39 } ], "currentCostumeIndex": 0, @@ -9560,7 +9972,7 @@ }, { "name": "Max", - "md5": "e10cca3bdbc09d039c2f937574f7a6ea.svg", + "md5": "5cc168bddf4561e6e7f1d30845d6efbf.svg", "type": "sprite", "tags": [ "sports", @@ -9577,46 +9989,46 @@ "objName": "Max", "sounds": [ { - "soundName": "pop", + "soundName": "basketball bounce", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1727f65b5f22d151685b8e5917456a60.wav", + "sampleCount": 8099, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "max-a", "baseLayerID": -1, - "baseLayerMD5": "e10cca3bdbc09d039c2f937574f7a6ea.svg", + "baseLayerMD5": "5cc168bddf4561e6e7f1d30845d6efbf.svg", "bitmapResolution": 1, - "rotationCenterX": 82, - "rotationCenterY": 68 + "rotationCenterX": 70, + "rotationCenterY": 61 }, { "costumeName": "max-b", "baseLayerID": -1, - "baseLayerMD5": "6d8ee139a741cf945d600a8cef0ea2e6.svg", + "baseLayerMD5": "8e2ad5735e26c5544b6b1a057f4efa80.svg", "bitmapResolution": 1, - "rotationCenterX": 82, - "rotationCenterY": 68 + "rotationCenterX": 71, + "rotationCenterY": 64 }, { "costumeName": "max-c", "baseLayerID": -1, - "baseLayerMD5": "aa66109994d27de02711f6a0ef6de9ec.svg", + "baseLayerMD5": "261a40ebecfe135308befd79c6c07fd5.svg", "bitmapResolution": 1, - "rotationCenterX": 82, - "rotationCenterY": 68 + "rotationCenterX": 46, + "rotationCenterY": 59 }, { "costumeName": "max-d", "baseLayerID": -1, - "baseLayerMD5": "a0dbf509d542c7eff6d2ddfc9c9410f1.svg", + "baseLayerMD5": "4b6eecd411daddef6422fa3a5850cd52.svg", "bitmapResolution": 1, - "rotationCenterX": 82, - "rotationCenterY": 68 + "rotationCenterX": 37, + "rotationCenterY": 59 } ], "currentCostumeIndex": 0, @@ -9642,19 +10054,19 @@ ], "info": [ 0, - 2, + 4, 1 ], "json": { "objName": "Mermaid", "sounds": [ { - "soundName": "pop", + "soundName": "ocean wave", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "c904610d770398b98872a708a2f75611.wav", + "sampleCount": 99206, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -9673,67 +10085,27 @@ "bitmapResolution": 1, "rotationCenterX": 92, "rotationCenterY": 130 - } - ], - "currentCostumeIndex": 0, - "scratchX": -110, - "scratchY": -21, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, - { - "name": "Mermaid-swimming", - "md5": "9f973b89b68f7d8147f157cbac8af341.svg", - "type": "sprite", - "tags": [ - "fantasy", - "people", - "underwater", - "ipzy" - ], - "info": [ - 0, - 2, - 1 - ], - "json": { - "objName": "Mermaid-swimming", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "mermaid-swim-a", - "baseLayerID": -1, - "baseLayerMD5": "9f973b89b68f7d8147f157cbac8af341.svg", - "bitmapResolution": 1, - "rotationCenterX": 150, - "rotationCenterY": 115 }, { - "costumeName": "mermaid-swim-b", + "costumeName": "mermaid-c", "baseLayerID": -1, "baseLayerMD5": "2295784bb8e6354bfa7676089235cb9f.svg", "bitmapResolution": 1, "rotationCenterX": 150, "rotationCenterY": 115 + }, + { + "costumeName": "mermaid-d", + "baseLayerID": -1, + "baseLayerMD5": "9f973b89b68f7d8147f157cbac8af341.svg", + "bitmapResolution": 1, + "rotationCenterX": 150, + "rotationCenterY": 115 } ], "currentCostumeIndex": 0, - "scratchX": 141, - "scratchY": -8, + "scratchX": -110, + "scratchY": -21, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -9759,7 +10131,7 @@ "objName": "Microphone", "sounds": [ { - "soundName": "bass beatbox", + "soundName": "Bass Beatbox", "soundID": -1, "md5": "28153621d293c86da0b246d314458faf.wav", "sampleCount": 6720, @@ -9767,7 +10139,7 @@ "format": "" }, { - "soundName": "clap beatbox", + "soundName": "Clap Beatbox", "soundID": -1, "md5": "abc70bb390f8e55f22f32265500d814a.wav", "sampleCount": 4224, @@ -9775,7 +10147,7 @@ "format": "" }, { - "soundName": "hi beatbox", + "soundName": "Hi Beatbox", "soundID": -1, "md5": "5a07847bf246c227204728b05a3fc8f3.wav", "sampleCount": 5856, @@ -9783,7 +10155,7 @@ "format": "" }, { - "soundName": "scratch beatbox", + "soundName": "Scratch Beatbox", "soundID": -1, "md5": "859249563a7b1fc0f6e92e36d1db81c7.wav", "sampleCount": 11552, @@ -9791,7 +10163,7 @@ "format": "" }, { - "soundName": "snare beatbox", + "soundName": "Snare Beatbox", "soundID": -1, "md5": "c642c4c00135d890998f351faec55498.wav", "sampleCount": 5630, @@ -9799,7 +10171,7 @@ "format": "adpcm" }, { - "soundName": "snare beatbox2", + "soundName": "Snare Beatbox2", "soundID": -1, "md5": "7ede1382b578d8fc32850b48d082d914.wav", "sampleCount": 4960, @@ -9807,7 +10179,7 @@ "format": "" }, { - "soundName": "wah beatbox", + "soundName": "Wah Beatbox", "soundID": -1, "md5": "9021b7bb06f2399f18e2db4fb87095dc.wav", "sampleCount": 6624, @@ -9815,7 +10187,7 @@ "format": "" }, { - "soundName": "crash beatbox", + "soundName": "Crash Beatbox", "soundID": -1, "md5": "725e29369e9138a43f11e0e5eb3eb562.wav", "sampleCount": 26883, @@ -9823,7 +10195,7 @@ "format": "" }, { - "soundName": "wub beatbox", + "soundName": "Wub Beatbox", "soundID": -1, "md5": "e1f32c057411da4237181ce72ae15d23.wav", "sampleCount": 7392, @@ -9862,7 +10234,7 @@ }, { "name": "Milk", - "md5": "e6a7964bc4ea38c79a5a31d6ddfb5ba9.svg", + "md5": "1b8ec45254b7c40d86e7a33818b77e35.svg", "type": "sprite", "tags": [ "food", @@ -9878,59 +10250,59 @@ "objName": "Milk", "sounds": [ { - "soundName": "pop", + "soundName": "glug", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "5606722c6105f3c58f9689a958f5c45f.wav", + "sampleCount": 12100, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "milk-a", "baseLayerID": -1, - "baseLayerMD5": "e6a7964bc4ea38c79a5a31d6ddfb5ba9.svg", + "baseLayerMD5": "1b8ec45254b7c40d86e7a33818b77e35.svg", "bitmapResolution": 1, - "rotationCenterX": 68, - "rotationCenterY": 71 + "rotationCenterX": 32, + "rotationCenterY": 63 }, { "costumeName": "milk-b", "baseLayerID": -1, - "baseLayerMD5": "82d4c1855fe0d400433c7344fb2af3b5.svg", + "baseLayerMD5": "2ba802dcac09a81f9d82cc9d3b95d71b.svg", "bitmapResolution": 1, - "rotationCenterX": 68, - "rotationCenterY": 71 + "rotationCenterX": 42, + "rotationCenterY": 64 }, { "costumeName": "milk-c", "baseLayerID": -1, - "baseLayerMD5": "50afc991b6fdad4b6547ba98ecf8a6af.svg", + "baseLayerMD5": "287b322e939ad54691aef4876c48f422.svg", "bitmapResolution": 1, "rotationCenterX": 47, - "rotationCenterY": 44 + "rotationCenterY": 56 }, { "costumeName": "milk-d", "baseLayerID": -1, - "baseLayerMD5": "8fc7606a176149d225a541a04fa67473.svg", + "baseLayerMD5": "df4f5d863fa680331aa6c8f867b9fcbb.svg", "bitmapResolution": 1, - "rotationCenterX": 68, - "rotationCenterY": 71 + "rotationCenterX": 45, + "rotationCenterY": 64 }, { "costumeName": "milk-e", "baseLayerID": -1, - "baseLayerMD5": "f2373d449b1226c44436dced422c2935.svg", + "baseLayerMD5": "e642678d42d5e74fe49e6ceb5538724e.svg", "bitmapResolution": 1, - "rotationCenterX": 68, - "rotationCenterY": 71 + "rotationCenterX": 35, + "rotationCenterY": 45 } ], "currentCostumeIndex": 0, - "scratchX": -2, - "scratchY": -85, + "scratchX": -4, + "scratchY": -82, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -10031,18 +10403,26 @@ "info": [ 0, 3, - 1 + 2 ], "json": { "objName": "Monkey", "sounds": [ { - "soundName": "chee chee", + "soundName": "Chee Chee", "soundID": -1, "md5": "25f4826cdd61e0a1c623ec2324c16ca0.wav", "sampleCount": 34560, "rate": 22050, "format": "" + }, + { + "soundName": "Chomp", + "soundID": -1, + "md5": "0b1e3033140d094563248e61de4039e5.wav", + "sampleCount": 2912, + "rate": 11025, + "format": "" } ], "costumes": [ @@ -10082,6 +10462,86 @@ "spriteInfo": {} } }, + { + "name": "Motorcycle", + "md5": "5065a917834923efc3e0c112d5791e80.svg", + "type": "sprite", + "tags": [ + "city", + "vehicle", + "bike", + "motorcycle" + ], + "info": [ + 0, + 4, + 2 + ], + "json": { + "objName": "Motorcycle", + "sounds": [ + { + "soundName": "clown honk", + "soundID": -1, + "md5": "ec66961f188e9b8a9c75771db744d096.wav", + "sampleCount": 9009, + "rate": 22050, + "format": "adpcm" + }, + { + "soundName": "car vroom", + "soundID": -1, + "md5": "ead1da4a87ff6cb53441142f7ac37b8f.wav", + "sampleCount": 43358, + "rate": 22050, + "format": "adpcm" + } + ], + "costumes": [ + { + "costumeName": "Motorcycle-a", + "baseLayerID": -1, + "baseLayerMD5": "5065a917834923efc3e0c112d5791e80.svg", + "bitmapResolution": 1, + "rotationCenterX": 52, + "rotationCenterY": 44 + }, + { + "costumeName": "Motorcycle-b", + "baseLayerID": -1, + "baseLayerMD5": "fe0923cb08c0b7c373de1daf8b74de74.svg", + "bitmapResolution": 1, + "rotationCenterX": 52, + "rotationCenterY": 44 + }, + { + "costumeName": "Motorcycle-c", + "baseLayerID": -1, + "baseLayerMD5": "6af27b2b5e1eeae9e60702630e6909a8.svg", + "bitmapResolution": 1, + "rotationCenterX": 52, + "rotationCenterY": 44 + }, + { + "costumeName": "Motorcycle-d", + "baseLayerID": -1, + "baseLayerMD5": "c5d78ac1de3a7f786a4ca6dc0fe087d4.svg", + "bitmapResolution": 1, + "rotationCenterX": 52, + "rotationCenterY": 44 + } + ], + "currentCostumeIndex": 0, + "scratchX": 181, + "scratchY": -133, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, { "name": "Mouse1", "md5": "e1f0c26afecbe9d4b9923d8e6bf489a8.svg", @@ -10153,10 +10613,10 @@ "objName": "Muffin", "sounds": [ { - "soundName": "pop", + "soundName": "Chomp", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, + "md5": "0b1e3033140d094563248e61de4039e5.wav", + "sampleCount": 2912, "rate": 11025, "format": "" } @@ -10320,18 +10780,26 @@ "info": [ 0, 5, - 1 + 2 ], "json": { "objName": "Octopus", "sounds": [ { - "soundName": "pop", + "soundName": "splash", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "6aed5e38d40b87a21d893d26fa2858c0.wav", + "sampleCount": 46080, + "rate": 22050, + "format": "adpcm" + }, + { + "soundName": "ocean wave", + "soundID": -1, + "md5": "c904610d770398b98872a708a2f75611.wav", + "sampleCount": 99206, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -10490,7 +10958,7 @@ }, { "name": "Outfielder", - "md5": "fab7218666ba49eae992664acab53159.svg", + "md5": "d2e2e4a9249c4cbe479e431a4c985286.svg", "type": "sprite", "tags": [ "baseball", @@ -10519,38 +10987,38 @@ { "costumeName": "outfielder-a", "baseLayerID": -1, - "baseLayerMD5": "fab7218666ba49eae992664acab53159.svg", + "baseLayerMD5": "d2e2e4a9249c4cbe479e431a4c985286.svg", "bitmapResolution": 1, - "rotationCenterX": 38, - "rotationCenterY": 61 + "rotationCenterX": 42, + "rotationCenterY": 78 }, { "costumeName": "outfielder-b", "baseLayerID": -1, - "baseLayerMD5": "a86813067f26947adb68897b870b1b7e.svg", + "baseLayerMD5": "5a960b109ae5dbe3c3ea5555894e8c13.svg", "bitmapResolution": 1, - "rotationCenterX": 38, - "rotationCenterY": 57 + "rotationCenterX": 42, + "rotationCenterY": 74 }, { "costumeName": "outfielder-c", "baseLayerID": -1, - "baseLayerMD5": "f6f1d2b7c6b50cbdac8cdbb55c872114.svg", + "baseLayerMD5": "480ab4ce1ab5586e829e1cf804824c8b.svg", "bitmapResolution": 1, - "rotationCenterX": 46, - "rotationCenterY": 80 + "rotationCenterX": 67, + "rotationCenterY": 97 }, { "costumeName": "outfielder-d", "baseLayerID": -1, - "baseLayerMD5": "c6fb3a8079bce9630ab9499926c16de9.svg", + "baseLayerMD5": "e5803b5601e0aafa8d0b227f1b1ae742.svg", "bitmapResolution": 1, - "rotationCenterX": 79, - "rotationCenterY": 96 + "rotationCenterX": 130, + "rotationCenterY": 114 } ], "currentCostumeIndex": 0, - "scratchX": -173, + "scratchX": -156, "scratchY": -28, "scale": 1, "direction": 90, @@ -10562,7 +11030,7 @@ }, { "name": "Owl", - "md5": "a312273b198fcacf68976e3cc74fadb4.svg", + "md5": "5348f1ea36a645322e88796545ca8ca1.svg", "type": "sprite", "tags": [ "animals", @@ -10590,26 +11058,26 @@ { "costumeName": "owl-a", "baseLayerID": -1, - "baseLayerMD5": "a312273b198fcacf68976e3cc74fadb4.svg", + "baseLayerMD5": "5348f1ea36a645322e88796545ca8ca1.svg", "bitmapResolution": 1, - "rotationCenterX": 113, - "rotationCenterY": 54 + "rotationCenterX": 24, + "rotationCenterY": 40 }, { "costumeName": "owl-b", "baseLayerID": -1, - "baseLayerMD5": "c9916dcfe67302367b05be7f3e5c5ddf.svg", + "baseLayerMD5": "3d8ba4198a03e484ed63bde584fd5bc1.svg", "bitmapResolution": 1, - "rotationCenterX": 113, - "rotationCenterY": 54 + "rotationCenterX": 44, + "rotationCenterY": 46 }, { "costumeName": "owl-c", "baseLayerID": -1, - "baseLayerMD5": "8ec3a2507f1d6dc9b39f7ae5a1ebfdd3.svg", + "baseLayerMD5": "4e81445e70169f074ad50473a78f3381.svg", "bitmapResolution": 1, - "rotationCenterX": 113, - "rotationCenterY": 54 + "rotationCenterX": 109, + "rotationCenterY": 41 } ], "currentCostumeIndex": 0, @@ -10738,7 +11206,8 @@ "type": "sprite", "tags": [ "fashion", - "pants" + "pants", + "clothing" ], "info": [ 0, @@ -10808,7 +11277,7 @@ "objName": "Parrot", "sounds": [ { - "soundName": "bird", + "soundName": "Bird", "soundID": -1, "md5": "18bd4b634a3f992a16b30344c7d810e0.wav", "sampleCount": 3840, @@ -10845,6 +11314,68 @@ "spriteInfo": {} } }, + { + "name": "Party Hats", + "md5": "f3ee428806f9fa6d396e30a8d6d0e741.svg", + "type": "sprite", + "tags": [ + "fashion", + "holiday" + ], + "info": [ + 0, + 3, + 1 + ], + "json": { + "objName": "Party Hats", + "sounds": [ + { + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + } + ], + "costumes": [ + { + "costumeName": "Party Hat-a", + "baseLayerID": -1, + "baseLayerMD5": "f3ee428806f9fa6d396e30a8d6d0e741.svg", + "bitmapResolution": 1, + "rotationCenterX": 24, + "rotationCenterY": 55 + }, + { + "costumeName": "Party Hat-b", + "baseLayerID": -1, + "baseLayerMD5": "71b51d9680be4aa946eb6642bc044791.svg", + "bitmapResolution": 1, + "rotationCenterX": 40, + "rotationCenterY": 61 + }, + { + "costumeName": "Party Hat-e", + "baseLayerID": -1, + "baseLayerMD5": "e69516f2a9bb6d632fdaba4fadd8f8f1.svg", + "bitmapResolution": 1, + "rotationCenterX": 52, + "rotationCenterY": 44 + } + ], + "currentCostumeIndex": 0, + "scratchX": -80, + "scratchY": -31, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, { "name": "Pencil", "md5": "4495fcb0443cebc5d43e66243a88f1ac.svg", @@ -10901,11 +11432,11 @@ }, { "name": "Penguin", - "md5": "6d85d016129c34c90ef269933a72a7a7.svg", + "md5": "0e78708b8988802d2209a34b50292bd3.svg", "type": "sprite", "tags": [ "animals", - "bird", + "penguin", "cold", "north pole", "south pole", @@ -10935,7 +11466,7 @@ { "costumeName": "penguin-a", "baseLayerID": -1, - "baseLayerMD5": "6d85d016129c34c90ef269933a72a7a7.svg", + "baseLayerMD5": "0e78708b8988802d2209a34b50292bd3.svg", "bitmapResolution": 1, "rotationCenterX": 36, "rotationCenterY": 46 @@ -10943,17 +11474,17 @@ { "costumeName": "penguin-b", "baseLayerID": -1, - "baseLayerMD5": "e93ef702208fad2776a34f57c1c746b1.svg", + "baseLayerMD5": "43af2a0b7f922ec5dc94217b2f3e08e4.svg", "bitmapResolution": 1, - "rotationCenterX": 36, + "rotationCenterX": 26, "rotationCenterY": 46 }, { "costumeName": "penguin-c", "baseLayerID": -1, - "baseLayerMD5": "c6d6ec90b897855831a73b3b3b8f202a.svg", + "baseLayerMD5": "4f35c3940957e3ffc2e83a2565942171.svg", "bitmapResolution": 1, - "rotationCenterX": 36, + "rotationCenterX": 35, "rotationCenterY": 46 } ], @@ -10980,19 +11511,19 @@ ], "info": [ 0, - 3, + 4, 1 ], "json": { "objName": "Penguin 2", "sounds": [ { - "soundName": "pop", + "soundName": "Chirp", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "3b8236bbb288019d93ae38362e865972.wav", + "sampleCount": 5301, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -11007,13 +11538,21 @@ { "costumeName": "penguin2-b", "baseLayerID": -1, + "baseLayerMD5": "b224a582395e0847c2ef4eefcfbc4546.svg", + "bitmapResolution": 1, + "rotationCenterX": 54, + "rotationCenterY": 61 + }, + { + "costumeName": "penguin2-c", + "baseLayerID": -1, "baseLayerMD5": "35fec7aa5f60cca945fe0615413f1f08.svg", "bitmapResolution": 1, "rotationCenterX": 48, "rotationCenterY": 62 }, { - "costumeName": "penguin2-c", + "costumeName": "penguin2-d", "baseLayerID": -1, "baseLayerMD5": "18fa51a64ebd5518f0c5c465525346e5.svg", "bitmapResolution": 1, @@ -11174,7 +11713,7 @@ }, { "name": "Pitcher", - "md5": "a8694f8f7867d95c62121e60cb8b4812.svg", + "md5": "24008f02de4b0d999d4526d34dec8b76.svg", "type": "sprite", "tags": [ "baseball", @@ -11203,39 +11742,39 @@ { "costumeName": "pitcher-a", "baseLayerID": -1, - "baseLayerMD5": "a8694f8f7867d95c62121e60cb8b4812.svg", + "baseLayerMD5": "24008f02de4b0d999d4526d34dec8b76.svg", "bitmapResolution": 1, "rotationCenterX": 42, - "rotationCenterY": 63 + "rotationCenterY": 74 }, { "costumeName": "pitcher-b", "baseLayerID": -1, - "baseLayerMD5": "13f363056c706870ede82798e6a8bcf6.svg", + "baseLayerMD5": "4644ab73c577ed878e3ccaa3d89ccc2b.svg", "bitmapResolution": 1, - "rotationCenterX": 70, - "rotationCenterY": 86 + "rotationCenterX": 75, + "rotationCenterY": 97 }, { "costumeName": "pitcher-c", "baseLayerID": -1, - "baseLayerMD5": "6e26bd644ecd8d1f1606261d1b8f6a75.svg", + "baseLayerMD5": "f46aa0b114a9977c52eeefa252953e2b.svg", "bitmapResolution": 1, - "rotationCenterX": 62, - "rotationCenterY": 87 + "rotationCenterX": 69, + "rotationCenterY": 98 }, { "costumeName": "pitcher-d", "baseLayerID": -1, - "baseLayerMD5": "120d823290171ad83b6b328a5ffaf60e.svg", + "baseLayerMD5": "d75a06e24f717e8647db27e94618e693.svg", "bitmapResolution": 1, - "rotationCenterX": 82, - "rotationCenterY": 46 + "rotationCenterX": 84, + "rotationCenterY": 57 } ], "currentCostumeIndex": 0, - "scratchX": -5, - "scratchY": -79, + "scratchX": -6, + "scratchY": -88, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -11291,10 +11830,12 @@ }, { "name": "Polar Bear", - "md5": "c96435a1b9ad2ed37b2c9c5aac6d1fb4.svg", + "md5": "b1d892d9f8a897bc6a446e2ff766ba74.svg", "type": "sprite", "tags": [ "animals", + "bear", + "polar bear", "cold", "north pole", "south pole", @@ -11324,26 +11865,26 @@ { "costumeName": "polar bear-a", "baseLayerID": -1, - "baseLayerMD5": "c96435a1b9ad2ed37b2c9c5aac6d1fb4.svg", + "baseLayerMD5": "b1d892d9f8a897bc6a446e2ff766ba74.svg", "bitmapResolution": 1, "rotationCenterX": 104, - "rotationCenterY": 46 + "rotationCenterY": 42 }, { "costumeName": "polar bear-b", "baseLayerID": -1, - "baseLayerMD5": "4f9b9061414deedeeebd369a496aba07.svg", + "baseLayerMD5": "54728e795213e67ac7cecca1c32fe293.svg", "bitmapResolution": 1, - "rotationCenterX": 104, - "rotationCenterY": 46 + "rotationCenterX": 87, + "rotationCenterY": 47 }, { "costumeName": "polar bear-c", "baseLayerID": -1, - "baseLayerMD5": "defad238b915f766449e9dcb719bb6e0.svg", + "baseLayerMD5": "7d5812e0aa1cde33771f9270ff521cfe.svg", "bitmapResolution": 1, "rotationCenterX": 104, - "rotationCenterY": 46 + "rotationCenterY": 43 } ], "currentCostumeIndex": 0, @@ -11375,12 +11916,12 @@ "objName": "Potion", "sounds": [ { - "soundName": "pop", + "soundName": "Magic Spell", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1cb60ecdb1075c8769cb346d5c2a22c7.wav", + "sampleCount": 43077, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -11487,12 +12028,12 @@ "objName": "Princess", "sounds": [ { - "soundName": "pop", + "soundName": "magic spell", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1cb60ecdb1075c8769cb346d5c2a22c7.wav", + "sampleCount": 43077, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -11568,12 +12109,12 @@ "objName": "Pufferfish", "sounds": [ { - "soundName": "pop", + "soundName": "ocean wave", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "c904610d770398b98872a708a2f75611.wav", + "sampleCount": 99206, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -11621,96 +12162,6 @@ "spriteInfo": {} } }, - { - "name": "Pumpkin Costume", - "md5": "6f7a5a88e6201082913a099438d626ef.svg", - "type": "sprite", - "tags": [ - "fashion" - ], - "info": [ - 0, - 1, - 1 - ], - "json": { - "objName": "Pumpkin Costume", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "pumpkin-costume", - "baseLayerID": -1, - "baseLayerMD5": "6f7a5a88e6201082913a099438d626ef.svg", - "bitmapResolution": 1, - "rotationCenterX": 39, - "rotationCenterY": 63 - } - ], - "currentCostumeIndex": 0, - "scratchX": -169, - "scratchY": 13, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, - { - "name": "Pumpkin Hat", - "md5": "22bdeaf0d8421ffb70775ed77070bee7.svg", - "type": "sprite", - "tags": [ - "fashion" - ], - "info": [ - 0, - 1, - 1 - ], - "json": { - "objName": "Pumpkin Hat", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "pumpkin-hat", - "baseLayerID": -1, - "baseLayerMD5": "22bdeaf0d8421ffb70775ed77070bee7.svg", - "bitmapResolution": 1, - "rotationCenterX": 28, - "rotationCenterY": 24 - } - ], - "currentCostumeIndex": 0, - "scratchX": -172, - "scratchY": 126, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, { "name": "Puppy", "md5": "2768d9e44a0aab055856d301bbc2b04e.png", @@ -11864,6 +12315,86 @@ "spriteInfo": {} } }, + { + "name": "Radio", + "md5": "d2266f31369a97fdf7ec37090bf0a968.svg", + "type": "sprite", + "tags": [ + "radio", + "music", + "beatbox", + "bedrorom" + ], + "info": [ + 0, + 2, + 4 + ], + "json": { + "objName": "Radio", + "sounds": [ + { + "soundName": "scratch beatbox", + "soundID": -1, + "md5": "859249563a7b1fc0f6e92e36d1db81c7.wav", + "sampleCount": 11552, + "rate": 22050, + "format": "" + }, + { + "soundName": "snare beatbox2", + "soundID": -1, + "md5": "7ede1382b578d8fc32850b48d082d914.wav", + "sampleCount": 4960, + "rate": 22050, + "format": "" + }, + { + "soundName": "wah beatbox", + "soundID": -1, + "md5": "9021b7bb06f2399f18e2db4fb87095dc.wav", + "sampleCount": 6624, + "rate": 22050, + "format": "" + }, + { + "soundName": "bass beatbox", + "soundID": -1, + "md5": "28153621d293c86da0b246d314458faf.wav", + "sampleCount": 6720, + "rate": 22050, + "format": "" + } + ], + "costumes": [ + { + "costumeName": "Radio-a", + "baseLayerID": -1, + "baseLayerMD5": "d2266f31369a97fdf7ec37090bf0a968.svg", + "bitmapResolution": 1, + "rotationCenterX": 52, + "rotationCenterY": 38 + }, + { + "costumeName": "Radio-b", + "baseLayerID": -1, + "baseLayerMD5": "cd4cff5586d0167fd992772360bddf68.svg", + "bitmapResolution": 1, + "rotationCenterX": 51, + "rotationCenterY": 84 + } + ], + "currentCostumeIndex": 0, + "scratchX": -164, + "scratchY": -96, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, { "name": "Rainbow", "md5": "680a806bd87a28c8b25b5f9b6347f022.svg", @@ -11914,7 +12445,7 @@ }, { "name": "Referee", - "md5": "0959403aeb134ed2932a85c77e261122.svg", + "md5": "7caaa6c83b0f947268866b48fbf34ed9.svg", "type": "sprite", "tags": [ "sports", @@ -11931,51 +12462,51 @@ "objName": "Referee", "sounds": [ { - "soundName": "pop", + "soundName": "referee whistle", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "8468b9b3f11a665ee4d215afd8463b97.wav", + "sampleCount": 14034, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "referee-a", "baseLayerID": -1, - "baseLayerMD5": "0959403aeb134ed2932a85c77e261122.svg", + "baseLayerMD5": "7caaa6c83b0f947268866b48fbf34ed9.svg", "bitmapResolution": 1, - "rotationCenterX": 76, - "rotationCenterY": 68 + "rotationCenterX": 44, + "rotationCenterY": 63 }, { "costumeName": "referee-b", "baseLayerID": -1, - "baseLayerMD5": "866b9e1ad2e0584216dd45fe7d50d6f5.svg", + "baseLayerMD5": "b26bc52a8400fa3d51201e9ab4a4dbb2.svg", "bitmapResolution": 1, - "rotationCenterX": 76, - "rotationCenterY": 68 + "rotationCenterX": 44, + "rotationCenterY": 63 }, { "costumeName": "referee-c", "baseLayerID": -1, - "baseLayerMD5": "21a3869435fbd470ef60960a78b06b16.svg", + "baseLayerMD5": "f96a3af7f21c0b195b8f7a84146b6704.svg", "bitmapResolution": 1, - "rotationCenterX": 76, - "rotationCenterY": 68 + "rotationCenterX": 55, + "rotationCenterY": 62 }, { "costumeName": "referee-d", "baseLayerID": -1, - "baseLayerMD5": "5e037aca5446a7e57093e45fe6f18c9e.svg", + "baseLayerMD5": "d6795f2992d899611ee5889811801e42.svg", "bitmapResolution": 1, - "rotationCenterX": 76, - "rotationCenterY": 68 + "rotationCenterX": 50, + "rotationCenterY": 63 } ], "currentCostumeIndex": 0, - "scratchX": 170, - "scratchY": 8, + "scratchX": 112, + "scratchY": 28, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -12031,6 +12562,75 @@ "spriteInfo": {} } }, + { + "name": "Retro Robot", + "md5": "a17c1ce38c261395ae268f3b8a9037db.svg", + "type": "sprite", + "tags": [ + "robot" + ], + "info": [ + 0, + 3, + 2 + ], + "json": { + "objName": "Retro Robot", + "sounds": [ + { + "soundName": "computer beeps1", + "soundID": -1, + "md5": "1da43f6d52d0615da8a250e28100a80d.wav", + "sampleCount": 19200, + "rate": 11025, + "format": "" + }, + { + "soundName": "computer beeps2", + "soundID": -1, + "md5": "28c76b6bebd04be1383fe9ba4933d263.wav", + "sampleCount": 9536, + "rate": 11025, + "format": "" + } + ], + "costumes": [ + { + "costumeName": "Retro Robot a", + "baseLayerID": -1, + "baseLayerMD5": "a17c1ce38c261395ae268f3b8a9037db.svg", + "bitmapResolution": 1, + "rotationCenterX": 55, + "rotationCenterY": 86 + }, + { + "costumeName": "Retro Robot b", + "baseLayerID": -1, + "baseLayerMD5": "bfb5afe62358ef542f118299bb53d4b7.svg", + "bitmapResolution": 1, + "rotationCenterX": 52, + "rotationCenterY": 88 + }, + { + "costumeName": "Retro Robot c", + "baseLayerID": -1, + "baseLayerMD5": "f925e3bde178001133a11fa97847a9ae.svg", + "bitmapResolution": 1, + "rotationCenterX": 72, + "rotationCenterY": 91 + } + ], + "currentCostumeIndex": 0, + "scratchX": 50, + "scratchY": -72, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, { "name": "Ripley", "md5": "417ec9f25ad70281564e85e67c97aa08.svg", @@ -12130,7 +12730,7 @@ "info": [ 0, 4, - 2 + 3 ], "json": { "objName": "Robot", @@ -12143,6 +12743,14 @@ "rate": 11025, "format": "" }, + { + "soundName": "collect", + "soundID": -1, + "md5": "32514c51e03db680e9c63857b840ae78.wav", + "sampleCount": 13320, + "rate": 22050, + "format": "adpcm" + }, { "soundName": "buzz whir", "soundID": -1, @@ -12187,8 +12795,8 @@ } ], "currentCostumeIndex": 0, - "scratchX": -183, - "scratchY": 15, + "scratchX": -127, + "scratchY": 87, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -12199,7 +12807,7 @@ }, { "name": "Rocketship", - "md5": "7d51af7c52e137ef137012595bac928d.svg", + "md5": "85190bb07708855c0896224340e159ed.svg", "type": "sprite", "tags": [ "space", @@ -12243,47 +12851,47 @@ { "costumeName": "rocketship-a", "baseLayerID": -1, - "baseLayerMD5": "7d51af7c52e137ef137012595bac928d.svg", + "baseLayerMD5": "85190bb07708855c0896224340e159ed.svg", "bitmapResolution": 1, - "rotationCenterX": 66, - "rotationCenterY": 107 + "rotationCenterX": 63, + "rotationCenterY": 92 }, { "costumeName": "rocketship-b", "baseLayerID": -1, - "baseLayerMD5": "ae2634705b7a4fd31f4c1d1bb0e12545.svg", + "baseLayerMD5": "2c0f804073ec656a86bbc3ebdf18bf5d.svg", "bitmapResolution": 1, - "rotationCenterX": 53, - "rotationCenterY": 106 + "rotationCenterX": 54, + "rotationCenterY": 92 }, { "costumeName": "rocketship-c", "baseLayerID": -1, - "baseLayerMD5": "3d375cd033f1a7161cae56b114f4cfdb.svg", + "baseLayerMD5": "13a0accfa405979e4deee12e6eb9c41e.svg", "bitmapResolution": 1, - "rotationCenterX": 61, - "rotationCenterY": 107 + "rotationCenterX": 59, + "rotationCenterY": 91 }, { "costumeName": "rocketship-d", "baseLayerID": -1, - "baseLayerMD5": "a456964f32cd7e7bd83fe076bf29558b.svg", + "baseLayerMD5": "f4d0c6da1c167abbd8e86ff720ae24f9.svg", "bitmapResolution": 1, - "rotationCenterX": 63, - "rotationCenterY": 107 + "rotationCenterX": 65, + "rotationCenterY": 91 }, { "costumeName": "rocketship-e", "baseLayerID": -1, - "baseLayerMD5": "5b5cc9904ba63ca2801b61a73d4dcb7e.svg", + "baseLayerMD5": "6e58c447e994a38b51eeb105dff5711c.svg", "bitmapResolution": 1, "rotationCenterX": 71, - "rotationCenterY": 107 + "rotationCenterY": 111 } ], "currentCostumeIndex": 0, - "scratchX": 116, - "scratchY": 82, + "scratchX": 123, + "scratchY": 50, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -12340,7 +12948,7 @@ }, { "name": "Rooster", - "md5": "8a6f3145a24db10d5a2f90975ce8140c.svg", + "md5": "db49a3dc8db15ea2b92d7934488f996b.svg", "type": "sprite", "tags": [ "animals", @@ -12351,7 +12959,7 @@ "info": [ 0, 3, - 1 + 2 ], "json": { "objName": "Rooster", @@ -12363,37 +12971,45 @@ "sampleCount": 258, "rate": 11025, "format": "" + }, + { + "soundName": "rooster", + "soundID": -1, + "md5": "2e375acae2c7c0d655935a9de14b12f6.wav", + "sampleCount": 17110, + "rate": 11025, + "format": "" } ], "costumes": [ { "costumeName": "rooster-a", "baseLayerID": -1, - "baseLayerMD5": "8a6f3145a24db10d5a2f90975ce8140c.svg", + "baseLayerMD5": "db49a3dc8db15ea2b92d7934488f996b.svg", "bitmapResolution": 1, - "rotationCenterX": 55, - "rotationCenterY": 58 + "rotationCenterX": 59, + "rotationCenterY": 70 }, { "costumeName": "rooster-b", "baseLayerID": -1, - "baseLayerMD5": "6a14a205b117fdb0b7a6fad91a989e96.svg", + "baseLayerMD5": "c5aa68995a2ac203d6fe2e4dd9d86911.svg", "bitmapResolution": 1, - "rotationCenterX": 55, - "rotationCenterY": 59 + "rotationCenterX": 58, + "rotationCenterY": 70 }, { "costumeName": "rooster-c", "baseLayerID": -1, - "baseLayerMD5": "f23408efb8f47022da51c8fba6003cbb.svg", + "baseLayerMD5": "b7a5c9f9d387cc7d36d6ed935d2a46b1.svg", "bitmapResolution": 1, - "rotationCenterX": 55, - "rotationCenterY": 59 + "rotationCenterX": 54, + "rotationCenterY": 72 } ], "currentCostumeIndex": 0, - "scratchX": 125, - "scratchY": -99, + "scratchX": -6, + "scratchY": 3, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -12563,7 +13179,7 @@ "objName": "Saxophone", "sounds": [ { - "soundName": "C sax", + "soundName": "C Sax", "soundID": -1, "md5": "4d2c939d6953b5f241a27a62cf72de64.wav", "sampleCount": 9491, @@ -12571,7 +13187,7 @@ "format": "" }, { - "soundName": "D sax", + "soundName": "D Sax", "soundID": -1, "md5": "39f41954a73c0e15d842061e1a4c5e1d.wav", "sampleCount": 9555, @@ -12579,7 +13195,7 @@ "format": "" }, { - "soundName": "E sax", + "soundName": "E Sax", "soundID": -1, "md5": "3568b7dfe173fab6877a9ff1dcbcf1aa.wav", "sampleCount": 7489, @@ -12587,7 +13203,7 @@ "format": "" }, { - "soundName": "F sax", + "soundName": "F Sax", "soundID": -1, "md5": "2ae3083817bcd595e26ea2884b6684d5.wav", "sampleCount": 7361, @@ -12595,7 +13211,7 @@ "format": "adpcm" }, { - "soundName": "G sax", + "soundName": "G Sax", "soundID": -1, "md5": "cefba5de46adfe5702485e0934bb1e13.wav", "sampleCount": 7349, @@ -12603,7 +13219,7 @@ "format": "adpcm" }, { - "soundName": "A sax", + "soundName": "A Sax", "soundID": -1, "md5": "420991e0d6d99292c6d736963842536a.wav", "sampleCount": 6472, @@ -12611,7 +13227,7 @@ "format": "" }, { - "soundName": "B sax", + "soundName": "B Sax", "soundID": -1, "md5": "653ebe92d491b49ad5d8101d629f567b.wav", "sampleCount": 9555, @@ -12619,7 +13235,7 @@ "format": "" }, { - "soundName": "C2 sax", + "soundName": "C2 Sax", "soundID": -1, "md5": "ea8d34b18c3d8fe328cea201666458bf.wav", "sampleCount": 7349, @@ -12657,8 +13273,8 @@ } }, { - "name": "Scarf1", - "md5": "0c03668b4e26eba969ad803a4e810c78.svg", + "name": "Scarf", + "md5": "5d671a4eb5c42e365d3c0ba1b885d9d8.svg", "type": "sprite", "tags": [ "fashion", @@ -12666,11 +13282,11 @@ ], "info": [ 0, - 1, + 3, 1 ], "json": { - "objName": "Scarf1", + "objName": "Scarf", "sounds": [ { "soundName": "pop", @@ -12683,12 +13299,28 @@ ], "costumes": [ { - "costumeName": "scarf1", + "costumeName": "Scarf-a", + "baseLayerID": -1, + "baseLayerMD5": "5d671a4eb5c42e365d3c0ba1b885d9d8.svg", + "bitmapResolution": 1, + "rotationCenterX": 54, + "rotationCenterY": 23 + }, + { + "costumeName": "Scarf-b", "baseLayerID": -1, "baseLayerMD5": "0c03668b4e26eba969ad803a4e810c78.svg", "bitmapResolution": 1, "rotationCenterX": 54, "rotationCenterY": 23 + }, + { + "costumeName": "Scarf-c", + "baseLayerID": -1, + "baseLayerMD5": "ce66662165e2756070f1b12e0a7cb5db.svg", + "bitmapResolution": 1, + "rotationCenterX": 30, + "rotationCenterY": 44 } ], "currentCostumeIndex": 0, @@ -12702,52 +13334,6 @@ "spriteInfo": {} } }, - { - "name": "Scarf2", - "md5": "ce66662165e2756070f1b12e0a7cb5db.svg", - "type": "sprite", - "tags": [ - "fashion", - "purple" - ], - "info": [ - 0, - 1, - 1 - ], - "json": { - "objName": "Scarf2", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "scarf2", - "baseLayerID": -1, - "baseLayerMD5": "ce66662165e2756070f1b12e0a7cb5db.svg", - "bitmapResolution": 1, - "rotationCenterX": 23, - "rotationCenterY": 16 - } - ], - "currentCostumeIndex": 0, - "scratchX": 0, - "scratchY": 28, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, { "name": "Shark", "md5": "4ca6776e9c021e8b21c3346793c9361d.svg", @@ -12767,10 +13353,10 @@ "objName": "Shark", "sounds": [ { - "soundName": "pop", + "soundName": "chomp", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, + "md5": "0b1e3033140d094563248e61de4039e5.wav", + "sampleCount": 2912, "rate": 11025, "format": "" } @@ -12820,16 +13406,32 @@ "info": [ 0, 3, - 1 + 3 ], "json": { "objName": "Shark 2", "sounds": [ { - "soundName": "pop", + "soundName": "Water drop", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, + "md5": "e133e625fd367d269e76964d4b722fc2.wav", + "sampleCount": 15131, + "rate": 22050, + "format": "adpcm" + }, + { + "soundName": "Bite", + "soundID": -1, + "md5": "0039635b1d6853face36581784558454.wav", + "sampleCount": 7672, + "rate": 22050, + "format": "adpcm" + }, + { + "soundName": "Water drop", + "soundID": -1, + "md5": "0b1e3033140d094563248e61de4039e5.wav", + "sampleCount": 2912, "rate": 11025, "format": "" } @@ -12917,217 +13519,15 @@ "spriteInfo": {} } }, - { - "name": "Shirt Blouse", - "md5": "918a507af6bbae9e7f36f0d949900838.svg", - "type": "sprite", - "tags": [ - "fashion" - ], - "info": [ - 0, - 1, - 1 - ], - "json": { - "objName": "Shirt Blouse", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "shirt blouse", - "baseLayerID": -1, - "baseLayerMD5": "918a507af6bbae9e7f36f0d949900838.svg", - "bitmapResolution": 1, - "rotationCenterX": 35, - "rotationCenterY": 28 - } - ], - "currentCostumeIndex": 0, - "scratchX": -43, - "scratchY": -46, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, - { - "name": "Shirt Collar", - "md5": "b7ecf6503e27e9ab5c086eaf07d22b94.svg", - "type": "sprite", - "tags": [ - "fashion" - ], - "info": [ - 0, - 3, - 1 - ], - "json": { - "objName": "Shirt Collar", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "shirt collar-a", - "baseLayerID": -1, - "baseLayerMD5": "b7ecf6503e27e9ab5c086eaf07d22b94.svg", - "bitmapResolution": 1, - "rotationCenterX": 30, - "rotationCenterY": 57 - }, - { - "costumeName": "shirt collar-b", - "baseLayerID": -1, - "baseLayerMD5": "f1b20c3350e8a7e92a2fb1925ad4158b.svg", - "bitmapResolution": 1, - "rotationCenterX": 30, - "rotationCenterY": 57 - }, - { - "costumeName": "shirt collar-c", - "baseLayerID": -1, - "baseLayerMD5": "5f04b99416a794e04d0855f446780f18.svg", - "bitmapResolution": 1, - "rotationCenterX": 30, - "rotationCenterY": 57 - } - ], - "currentCostumeIndex": 0, - "scratchX": -74, - "scratchY": -4, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, - { - "name": "Shirt-T", - "md5": "5d7fa4f1788f03d2962f1624d6eac800.svg", - "type": "sprite", - "tags": [ - "fashion" - ], - "info": [ - 0, - 1, - 1 - ], - "json": { - "objName": "Shirt-T", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "shirt-t", - "baseLayerID": -1, - "baseLayerMD5": "5d7fa4f1788f03d2962f1624d6eac800.svg", - "bitmapResolution": 1, - "rotationCenterX": 39, - "rotationCenterY": 28 - } - ], - "currentCostumeIndex": 0, - "scratchX": 54, - "scratchY": -48, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, - { - "name": "Shirt2", - "md5": "5946e7a1e36c6d97ae47d41dd8595a41.svg", - "type": "sprite", - "tags": [ - "fashion" - ], - "info": [ - 0, - 2, - 1 - ], - "json": { - "objName": "Shirt2", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "shirt2-a", - "baseLayerID": -1, - "baseLayerMD5": "5946e7a1e36c6d97ae47d41dd8595a41.svg", - "bitmapResolution": 1, - "rotationCenterX": 39, - "rotationCenterY": 48 - }, - { - "costumeName": "shirt2-a2", - "baseLayerID": -1, - "baseLayerMD5": "5b78ab09592126b6bbe2d4907d203909.svg", - "bitmapResolution": 1, - "rotationCenterX": 39, - "rotationCenterY": 48 - } - ], - "currentCostumeIndex": 0, - "scratchX": 64, - "scratchY": -11, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, { "name": "Shoes", "md5": "4639a1af5bc91f1a6f14e822cd46972f.svg", "type": "sprite", "tags": [ "fashion", - "shoes" + "shoes", + "sneakers", + "clothing" ], "info": [ 0, @@ -13191,96 +13591,6 @@ "spriteInfo": {} } }, - { - "name": "Shoes1", - "md5": "ffab4cc284070b50ac317e515f59f7d8.svg", - "type": "sprite", - "tags": [ - "fashion" - ], - "info": [ - 0, - 1, - 1 - ], - "json": { - "objName": "Shoes1", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "shoes1", - "baseLayerID": -1, - "baseLayerMD5": "ffab4cc284070b50ac317e515f59f7d8.svg", - "bitmapResolution": 1, - "rotationCenterX": 36, - "rotationCenterY": 23 - } - ], - "currentCostumeIndex": 0, - "scratchX": 98, - "scratchY": 5, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, - { - "name": "Shoes2", - "md5": "1dc5d568d98405c370b91a230397a7f9.svg", - "type": "sprite", - "tags": [ - "fashion" - ], - "info": [ - 0, - 1, - 1 - ], - "json": { - "objName": "Shoes2", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "shoes2", - "baseLayerID": -1, - "baseLayerMD5": "1dc5d568d98405c370b91a230397a7f9.svg", - "bitmapResolution": 1, - "rotationCenterX": 40, - "rotationCenterY": 8 - } - ], - "currentCostumeIndex": 0, - "scratchX": 10, - "scratchY": 28, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, { "name": "Shorts", "md5": "d9b580c913c0d1d1a996dd733af91e68.svg", @@ -13391,56 +13701,9 @@ "spriteInfo": {} } }, - { - "name": "Skates", - "md5": "97936eccad5c8d86ea9008c5bbb50b63.svg", - "type": "sprite", - "tags": [ - "fashion", - "winter", - "sports" - ], - "info": [ - 0, - 1, - 1 - ], - "json": { - "objName": "Skates", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "skates", - "baseLayerID": -1, - "baseLayerMD5": "97936eccad5c8d86ea9008c5bbb50b63.svg", - "bitmapResolution": 1, - "rotationCenterX": 39, - "rotationCenterY": 19 - } - ], - "currentCostumeIndex": 0, - "scratchX": -21, - "scratchY": 22, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, { "name": "Skeleton", - "md5": "b6ddeecf18e40b99bde7e466759d5a5b.svg", + "md5": "e06f6eaf4fd27ff22f4c8207aa13c967.svg", "type": "sprite", "tags": [ "fantasy", @@ -13452,18 +13715,18 @@ ], "info": [ 0, - 5, + 4, 1 ], "json": { "objName": "Skeleton", "sounds": [ { - "soundName": "pop", + "soundName": "rattle", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, + "md5": "74f1c07e0bcd7811fd9d456a5f8667f8.wav", + "sampleCount": 13184, + "rate": 22050, "format": "" } ], @@ -13471,42 +13734,34 @@ { "costumeName": "skeleton-a", "baseLayerID": -1, - "baseLayerMD5": "b6ddeecf18e40b99bde7e466759d5a5b.svg", + "baseLayerMD5": "e06f6eaf4fd27ff22f4c8207aa13c967.svg", "bitmapResolution": 1, - "rotationCenterX": 75, - "rotationCenterY": 90 + "rotationCenterX": 59, + "rotationCenterY": 100 }, { "costumeName": "skeleton-b", "baseLayerID": -1, - "baseLayerMD5": "ba3f455026f5d7778af229ddddf83206.svg", + "baseLayerMD5": "06769ddf5fad85e101c7beda554d60b9.svg", "bitmapResolution": 1, - "rotationCenterX": 75, - "rotationCenterY": 90 - }, - { - "costumeName": "skeleton-c", - "baseLayerID": -1, - "baseLayerMD5": "af8fb44231f7891d52db5b4ad5c0f58b.svg", - "bitmapResolution": 1, - "rotationCenterX": 75, + "rotationCenterX": 69, "rotationCenterY": 90 }, { "costumeName": "skeleton-d", "baseLayerID": -1, - "baseLayerMD5": "355eb1591be72ea1117b6a4e153010b1.svg", + "baseLayerMD5": "155efd0dfdc59a0e2a546a4a7d068ece.svg", "bitmapResolution": 1, - "rotationCenterX": 75, - "rotationCenterY": 90 + "rotationCenterX": 51, + "rotationCenterY": 100 }, { "costumeName": "skeleton-e", "baseLayerID": -1, - "baseLayerMD5": "d3497858a3b5ad938e41a34bb8103c77.svg", + "baseLayerMD5": "c41a11468602522cdc6dec7043215f65.svg", "bitmapResolution": 1, - "rotationCenterX": 75, - "rotationCenterY": 90 + "rotationCenterX": 55, + "rotationCenterY": 89 } ], "currentCostumeIndex": 0, @@ -13677,7 +13932,7 @@ }, { "name": "Soccer Ball", - "md5": "81ff5ad24454e7a0f1f3ae863bb4dd25.svg", + "md5": "73eaf05fed60e9d65867508736f84569.svg", "type": "sprite", "tags": [ "sports", @@ -13694,22 +13949,22 @@ "objName": "Soccer Ball", "sounds": [ { - "soundName": "pop", + "soundName": "basketball bounce", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1727f65b5f22d151685b8e5917456a60.wav", + "sampleCount": 8099, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "soccer ball", "baseLayerID": -1, - "baseLayerMD5": "81ff5ad24454e7a0f1f3ae863bb4dd25.svg", + "baseLayerMD5": "73eaf05fed60e9d65867508736f84569.svg", "bitmapResolution": 1, - "rotationCenterX": 26, - "rotationCenterY": 26 + "rotationCenterX": 23, + "rotationCenterY": 22 } ], "currentCostumeIndex": 0, @@ -13765,7 +14020,7 @@ ], "sounds": [ { - "soundName": "drive around", + "soundName": "Drive Around", "soundID": -1, "md5": "a3a85fb8564b0266f50a9c091087b7aa.wav", "sampleCount": 44096, @@ -13773,7 +14028,7 @@ "format": "" }, { - "soundName": "scratchy beat", + "soundName": "Scratchy Beat", "soundID": -1, "md5": "289dc558e076971e74dd1a0bd55719b1.wav", "sampleCount": 44096, @@ -13781,7 +14036,7 @@ "format": "" }, { - "soundName": "drum jam", + "soundName": "Drum Jam", "soundID": -1, "md5": "8b5486ccc806e97e83049d25b071f7e4.wav", "sampleCount": 44288, @@ -13789,7 +14044,7 @@ "format": "" }, { - "soundName": "cymbal echo", + "soundName": "Cymbal Echo", "soundID": -1, "md5": "bb243badd1201b2607bf2513df10cd97.wav", "sampleCount": 44326, @@ -13797,7 +14052,7 @@ "format": "" }, { - "soundName": "drum satellite", + "soundName": "Drum Satellite", "soundID": -1, "md5": "079067d7909f791b29f8be1c00fc2131.wav", "sampleCount": 44096, @@ -13805,7 +14060,7 @@ "format": "" }, { - "soundName": "kick back", + "soundName": "Kick Back", "soundID": -1, "md5": "9cd340d9d568b1479f731e69e103b3ce.wav", "sampleCount": 44748, @@ -13813,7 +14068,7 @@ "format": "adpcm" }, { - "soundName": "drum funky", + "soundName": "Drum Funky", "soundID": -1, "md5": "fb56022366d21b299cbc3fd5e16000c2.wav", "sampleCount": 44748, @@ -13904,12 +14159,12 @@ "objName": "Star", "sounds": [ { - "soundName": "pop", + "soundName": "collect", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "32514c51e03db680e9c63857b840ae78.wav", + "sampleCount": 13320, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -13953,12 +14208,12 @@ "objName": "Starfish", "sounds": [ { - "soundName": "pop", + "soundName": "collect", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "32514c51e03db680e9c63857b840ae78.wav", + "sampleCount": 13320, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -14038,7 +14293,7 @@ }, { "name": "Strawberry", - "md5": "734556fb8e14740f2cbc971238b71d47.svg", + "md5": "b4cff4b414b5f7fbf25c7c45abfb6c62.svg", "type": "sprite", "tags": [ "food", @@ -14054,10 +14309,10 @@ "objName": "Strawberry", "sounds": [ { - "soundName": "pop", + "soundName": "chomp", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, + "md5": "0b1e3033140d094563248e61de4039e5.wav", + "sampleCount": 2912, "rate": 11025, "format": "" } @@ -14066,42 +14321,42 @@ { "costumeName": "strawberry-a", "baseLayerID": -1, - "baseLayerMD5": "734556fb8e14740f2cbc971238b71d47.svg", + "baseLayerMD5": "b4cff4b414b5f7fbf25c7c45abfb6c62.svg", "bitmapResolution": 1, - "rotationCenterX": 57, - "rotationCenterY": 58 + "rotationCenterX": 31, + "rotationCenterY": 47 }, { "costumeName": "strawberry-b", "baseLayerID": -1, - "baseLayerMD5": "77dadaa80bc5156f655c2196f57972f7.svg", + "baseLayerMD5": "ebb8720dc679772e3dcb1b49e0503aa4.svg", "bitmapResolution": 1, - "rotationCenterX": 57, - "rotationCenterY": 58 + "rotationCenterX": 43, + "rotationCenterY": 47 }, { "costumeName": "strawberry-c", "baseLayerID": -1, - "baseLayerMD5": "19f933b3cf3e8e150753f8fb9bb7a779.svg", + "baseLayerMD5": "278dc229a71e2fbff475908ca5c2483e.svg", "bitmapResolution": 1, - "rotationCenterX": 57, - "rotationCenterY": 58 + "rotationCenterX": 43, + "rotationCenterY": 47 }, { "costumeName": "strawberry-d", "baseLayerID": -1, - "baseLayerMD5": "8e8057da8457e6167de36b7d3d28b4bb.svg", + "baseLayerMD5": "68a023e80cb9eff2350374361c034f08.svg", "bitmapResolution": 1, - "rotationCenterX": 57, - "rotationCenterY": 58 + "rotationCenterX": 41, + "rotationCenterY": 47 }, { "costumeName": "strawberry-e", "baseLayerID": -1, - "baseLayerMD5": "5eb63e64b83f5aa5b75a55329a34efec.svg", + "baseLayerMD5": "9fef0cd91ba59163d74f4464b02824b8.svg", "bitmapResolution": 1, - "rotationCenterX": 57, - "rotationCenterY": 58 + "rotationCenterX": 32, + "rotationCenterY": 36 } ], "currentCostumeIndex": 0, @@ -14175,7 +14430,7 @@ ], "info": [ 0, - 1, + 2, 1 ], "json": { @@ -14192,53 +14447,15 @@ ], "costumes": [ { - "costumeName": "sunglasses1", + "costumeName": "sunglasses-a", "baseLayerID": -1, "baseLayerMD5": "424393e8705aeadcfecb8559ce4dcea2.svg", "bitmapResolution": 1, "rotationCenterX": 37, "rotationCenterY": 14 - } - ], - "currentCostumeIndex": 0, - "scratchX": -36, - "scratchY": 0, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, - { - "name": "Sunglasses2", - "md5": "3185d2295bbf2c5ebd0688c9e4f13076.svg", - "type": "sprite", - "tags": [ - "fashion", - "cool" - ], - "info": [ - 0, - 1, - 1 - ], - "json": { - "objName": "Sunglasses2", - "sounds": [ + }, { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "sunglasses2", + "costumeName": "sunglasses-b", "baseLayerID": -1, "baseLayerMD5": "3185d2295bbf2c5ebd0688c9e4f13076.svg", "bitmapResolution": 1, @@ -14247,8 +14464,8 @@ } ], "currentCostumeIndex": 0, - "scratchX": -57, - "scratchY": -25, + "scratchX": -36, + "scratchY": 0, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -14313,7 +14530,7 @@ }, { "name": "Takeout", - "md5": "0cfdefe0df1a032b90c8facd9f5dbe1f.svg", + "md5": "05c010b2425c4433353ff85d43ed7a1d.svg", "type": "sprite", "tags": [ "food", @@ -14340,42 +14557,42 @@ { "costumeName": "takeout-a", "baseLayerID": -1, - "baseLayerMD5": "0cfdefe0df1a032b90c8facd9f5dbe1f.svg", + "baseLayerMD5": "05c010b2425c4433353ff85d43ed7a1d.svg", "bitmapResolution": 1, - "rotationCenterX": 78, - "rotationCenterY": 61 + "rotationCenterX": 33, + "rotationCenterY": 41 }, { "costumeName": "takeout-b", "baseLayerID": -1, - "baseLayerMD5": "48b19c48e32c98a35836ee40e3a7accf.svg", + "baseLayerMD5": "26c3be261cbb0327eaa1afe0467c80d0.svg", "bitmapResolution": 1, - "rotationCenterX": 78, - "rotationCenterY": 61 + "rotationCenterX": 33, + "rotationCenterY": 42 }, { "costumeName": "takeout-c", "baseLayerID": -1, - "baseLayerMD5": "22d33d87883f8fb26c642eccc9b339f0.svg", + "baseLayerMD5": "801fa349b34531a15d467fef2b940f87.svg", "bitmapResolution": 1, - "rotationCenterX": 78, - "rotationCenterY": 61 + "rotationCenterX": 33, + "rotationCenterY": 53 }, { "costumeName": "takeout-d", "baseLayerID": -1, - "baseLayerMD5": "262f1a3730d669dc9d43b3853e397361.svg", + "baseLayerMD5": "24e21e2846b59cdf2c390ebe985b3c74.svg", "bitmapResolution": 1, - "rotationCenterX": 78, - "rotationCenterY": 61 + "rotationCenterX": 40, + "rotationCenterY": 42 }, { "costumeName": "takeout-e", "baseLayerID": -1, - "baseLayerMD5": "528e9df8c3bd173867be4143f8563e87.svg", + "baseLayerMD5": "95584a98e22df5ca7370f5c01ebe6d06.svg", "bitmapResolution": 1, - "rotationCenterX": 78, - "rotationCenterY": 61 + "rotationCenterX": 41, + "rotationCenterY": 35 } ], "currentCostumeIndex": 0, @@ -14391,7 +14608,7 @@ }, { "name": "Ten80 Dance", - "md5": "90ca0205c56c6c7f0e8892469de49bda.png", + "md5": "f60f99278455c843b7833fb7615428dd.png", "type": "sprite", "tags": [ "people", @@ -14418,7 +14635,7 @@ { "costumeName": "Ten80 stance", "baseLayerID": -1, - "baseLayerMD5": "90ca0205c56c6c7f0e8892469de49bda.png", + "baseLayerMD5": "f60f99278455c843b7833fb7615428dd.png", "bitmapResolution": 2, "rotationCenterX": 70, "rotationCenterY": 278 @@ -14433,7 +14650,7 @@ }, { "costumeName": "Ten80 top R step", - "baseLayerID": 4, + "baseLayerID": -1, "baseLayerMD5": "580fba92f23d5592200eb5a9079dc38f.png", "bitmapResolution": 2, "rotationCenterX": 200, @@ -14473,7 +14690,7 @@ }, { "costumeName": "Ten80 pop down", - "baseLayerID": 5, + "baseLayerID": -1, "baseLayerMD5": "fea7045c09073700b88fae8d4d257cd1.png", "bitmapResolution": 2, "rotationCenterX": 74, @@ -14857,9 +15074,73 @@ "spriteInfo": {} } }, + { + "name": "Truck", + "md5": "51404247db5cdbc082336dfa4c03d675.svg", + "type": "sprite", + "tags": [ + "truck", + "city", + "vehicle", + "car" + ], + "info": [ + 0, + 3, + 1 + ], + "json": { + "objName": "Truck", + "sounds": [ + { + "soundName": "toy honk", + "soundID": -1, + "md5": "67aadcd28620ecdcdee2ad8eeebefa20.wav", + "sampleCount": 10726, + "rate": 22050, + "format": "adpcm" + } + ], + "costumes": [ + { + "costumeName": "Truck-a", + "baseLayerID": -1, + "baseLayerMD5": "51404247db5cdbc082336dfa4c03d675.svg", + "bitmapResolution": 1, + "rotationCenterX": 174, + "rotationCenterY": 49 + }, + { + "costumeName": "Truck-b", + "baseLayerID": -1, + "baseLayerMD5": "1887b8233d1b0728f715a3985f0e01b6.svg", + "bitmapResolution": 1, + "rotationCenterX": 174, + "rotationCenterY": 58 + }, + { + "costumeName": "Truck-c", + "baseLayerID": -1, + "baseLayerMD5": "e45bc3f87d9f603e588abe14b63b8e90.svg", + "bitmapResolution": 1, + "rotationCenterX": 174, + "rotationCenterY": 58 + } + ], + "currentCostumeIndex": 0, + "scratchX": -74, + "scratchY": -46, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, { "name": "Trumpet", - "md5": "8fa7459ed5877bb14c6625e688be70e7.svg", + "md5": "36a48cc311fd28b4517e15d44b11587c.svg", "type": "sprite", "tags": [ "music", @@ -14874,7 +15155,7 @@ "objName": "Trumpet", "sounds": [ { - "soundName": "C trumpet", + "soundName": "C Trumpet", "soundID": -1, "md5": "8970afcdc4e47bb54959a81fe27522bd.wav", "sampleCount": 13118, @@ -14882,7 +15163,7 @@ "format": "" }, { - "soundName": "D trumpet", + "soundName": "D Trumpet", "soundID": -1, "md5": "0b1345b8fe2ba3076fedb4f3ae48748a.wav", "sampleCount": 12702, @@ -14890,7 +15171,7 @@ "format": "" }, { - "soundName": "E trumpet", + "soundName": "E Trumpet", "soundID": -1, "md5": "494295a92314cadb220945a6711c568c.wav", "sampleCount": 8680, @@ -14898,7 +15179,7 @@ "format": "adpcm" }, { - "soundName": "F trumpet", + "soundName": "F Trumpet", "soundID": -1, "md5": "5fa3108b119ca266029b4caa340a7cd0.wav", "sampleCount": 12766, @@ -14906,7 +15187,7 @@ "format": "" }, { - "soundName": "G trumpet", + "soundName": "G Trumpet", "soundID": -1, "md5": "e84afda25975f14b364118591538ccf4.wav", "sampleCount": 14640, @@ -14914,7 +15195,7 @@ "format": "" }, { - "soundName": "A trumpet", + "soundName": "A Trumpet", "soundID": -1, "md5": "d2dd6b4372ca17411965dc92d52b2172.wav", "sampleCount": 13911, @@ -14922,7 +15203,7 @@ "format": "" }, { - "soundName": "B trumpet", + "soundName": "B Trumpet", "soundID": -1, "md5": "cad2bc57729942ed9b605145fc9ea65d.wav", "sampleCount": 14704, @@ -14930,7 +15211,7 @@ "format": "" }, { - "soundName": "C2 trumpet", + "soundName": "C2 Trumpet", "soundID": -1, "md5": "df08249ed5446cc5e10b7ac62faac89b.wav", "sampleCount": 15849, @@ -14942,23 +15223,23 @@ { "costumeName": "trumpet-a", "baseLayerID": -1, - "baseLayerMD5": "8fa7459ed5877bb14c6625e688be70e7.svg", + "baseLayerMD5": "36a48cc311fd28b4517e15d44b11587c.svg", "bitmapResolution": 1, - "rotationCenterX": 37, - "rotationCenterY": 73 + "rotationCenterX": 57, + "rotationCenterY": 38 }, { "costumeName": "trumpet-b", "baseLayerID": -1, - "baseLayerMD5": "7cedda5ec925118f237094cd05381e5d.svg", + "baseLayerMD5": "c04cdfd5f05ff2817b6ab78027ef5d22.svg", "bitmapResolution": 1, - "rotationCenterX": 37, - "rotationCenterY": 73 + "rotationCenterX": 55, + "rotationCenterY": 37 } ], "currentCostumeIndex": 0, - "scratchX": 165, - "scratchY": 20, + "scratchX": 7, + "scratchY": 46, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -14969,7 +15250,7 @@ }, { "name": "Unicorn", - "md5": "bd2cf980966c13d5ad3ab403bae4bb05.svg", + "md5": "c491fd1867375aa0160b013788d188e5.svg", "type": "sprite", "tags": [ "fantasy", @@ -14985,27 +15266,27 @@ "objName": "Unicorn", "sounds": [ { - "soundName": "pop", + "soundName": "Magic Spell", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1cb60ecdb1075c8769cb346d5c2a22c7.wav", + "sampleCount": 43077, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { "costumeName": "unicorn", "baseLayerID": -1, - "baseLayerMD5": "bd2cf980966c13d5ad3ab403bae4bb05.svg", + "baseLayerMD5": "c491fd1867375aa0160b013788d188e5.svg", "bitmapResolution": 1, - "rotationCenterX": 100, - "rotationCenterY": 100 + "rotationCenterX": 91, + "rotationCenterY": 95 } ], "currentCostumeIndex": 0, - "scratchX": -8, - "scratchY": -47, + "scratchX": 5, + "scratchY": -72, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -15020,6 +15301,7 @@ "type": "sprite", "tags": [ "fantasy", + "animals", "horse", "horn", "rainbow" @@ -15033,12 +15315,12 @@ "objName": "Unicorn 2", "sounds": [ { - "soundName": "pop", + "soundName": "magic spell", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1cb60ecdb1075c8769cb346d5c2a22c7.wav", + "sampleCount": 43077, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -15063,13 +15345,14 @@ } }, { - "name": "Unicorn-running", - "md5": "c6456df027561c74b70b8b25ab83fec9.svg", + "name": "Unicorn Running", + "md5": "d938a2bfbac8f3caf713d189fd993af5.svg", "type": "sprite", "tags": [ "fantasy", "animals", - "ipzy" + "ipzy", + "walking" ], "info": [ 0, @@ -15077,123 +15360,70 @@ 1 ], "json": { - "objName": "Unicorn-running", + "objName": "Unicorn Running", "sounds": [ { - "soundName": "pop", + "soundName": "Magic Spell", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1cb60ecdb1075c8769cb346d5c2a22c7.wav", + "sampleCount": 43077, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ { - "costumeName": "unicorn-run-a", + "costumeName": "unicorn running-a", "baseLayerID": -1, - "baseLayerMD5": "c6456df027561c74b70b8b25ab83fec9.svg", + "baseLayerMD5": "d938a2bfbac8f3caf713d189fd993af5.svg", "bitmapResolution": 1, - "rotationCenterX": 130, - "rotationCenterY": 100 + "rotationCenterX": 118, + "rotationCenterY": 90 }, { - "costumeName": "unicorn-run-b", + "costumeName": "unicorn running-b", "baseLayerID": -1, - "baseLayerMD5": "f2d6c5b62b56d0cc51598126c74e0ace.svg", + "baseLayerMD5": "663618fcc0484879de6ad485b288c444.svg", "bitmapResolution": 1, - "rotationCenterX": 130, - "rotationCenterY": 100 + "rotationCenterX": 120, + "rotationCenterY": 89 }, { - "costumeName": "unicorn-run-c", + "costumeName": "unicorn running-c", "baseLayerID": -1, - "baseLayerMD5": "fba367b11a710bf542333649c4d68e1e.svg", + "baseLayerMD5": "ceb2f88fe182bc61af4e3864f8b46bba.svg", "bitmapResolution": 1, - "rotationCenterX": 130, - "rotationCenterY": 100 + "rotationCenterX": 121, + "rotationCenterY": 90 }, { - "costumeName": "unicorn-run-d", + "costumeName": "unicorn running-d", "baseLayerID": -1, - "baseLayerMD5": "20bad70a67e0c7ed3255c65780cdb2a8.svg", + "baseLayerMD5": "dddad08a2d03f406307ec7155f43c51c.svg", "bitmapResolution": 1, - "rotationCenterX": 130, - "rotationCenterY": 100 + "rotationCenterX": 120, + "rotationCenterY": 87 }, { - "costumeName": "unicorn-run-e", + "costumeName": "unicorn running-e", "baseLayerID": -1, - "baseLayerMD5": "39db08aafdde1542e755b8ead8df7a1f.svg", + "baseLayerMD5": "cf3a00e72f3fc20d9ad15379fdb7b424.svg", "bitmapResolution": 1, - "rotationCenterX": 130, - "rotationCenterY": 100 + "rotationCenterX": 119, + "rotationCenterY": 90 }, { - "costumeName": "unicorn-run-f", + "costumeName": "unicorn running-f", "baseLayerID": -1, - "baseLayerMD5": "d66d0625702433a4cbd7591e39676700.svg", + "baseLayerMD5": "264a70e872af2d3f6b2abad85daf82ae.svg", "bitmapResolution": 1, - "rotationCenterX": 130, - "rotationCenterY": 100 + "rotationCenterX": 117, + "rotationCenterY": 86 } ], "currentCostumeIndex": 0, - "scratchX": 93, - "scratchY": -66, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "visible": true, - "spriteInfo": {} - } - }, - { - "name": "Vest", - "md5": "f8285ca9564451c62a0e3d75b8a714e8.svg", - "type": "sprite", - "tags": [ - "fashon" - ], - "info": [ - 0, - 2, - 1 - ], - "json": { - "objName": "Vest", - "sounds": [ - { - "soundName": "pop", - "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - } - ], - "costumes": [ - { - "costumeName": "vest-a", - "baseLayerID": -1, - "baseLayerMD5": "f8285ca9564451c62a0e3d75b8a714e8.svg", - "bitmapResolution": 1, - "rotationCenterX": 18, - "rotationCenterY": 62 - }, - { - "costumeName": "vest-b", - "baseLayerID": -1, - "baseLayerMD5": "77d553eea3910ab8f5bac3d2da601061.svg", - "bitmapResolution": 1, - "rotationCenterX": 18, - "rotationCenterY": 62 - } - ], - "currentCostumeIndex": 0, - "scratchX": -37, - "scratchY": 28, + "scratchX": 78, + "scratchY": -73, "scale": 1, "direction": 90, "rotationStyle": "normal", @@ -15220,12 +15450,12 @@ "objName": "Wand", "sounds": [ { - "soundName": "pop", + "soundName": "Magic Spell", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1cb60ecdb1075c8769cb346d5c2a22c7.wav", + "sampleCount": 43077, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -15315,12 +15545,12 @@ "objName": "Watermelon", "sounds": [ { - "soundName": "meow", + "soundName": "Bite", "soundID": -1, - "md5": "83c36d806dc92327b9e7049a565c6bff.wav", - "sampleCount": 18688, + "md5": "0039635b1d6853face36581784558454.wav", + "sampleCount": 7672, "rate": 22050, - "format": "" + "format": "adpcm" } ], "costumes": [ @@ -15360,6 +15590,54 @@ "spriteInfo": {} } }, + { + "name": "Winter Hat", + "md5": "62678324450bac2154703e2978e8ab61.svg", + "type": "sprite", + "tags": [ + "fashion", + " clothing", + "winter", + "hat" + ], + "info": [ + 0, + 1, + 1 + ], + "json": { + "objName": "Winter Hat", + "sounds": [ + { + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + } + ], + "costumes": [ + { + "costumeName": "Winter Hat", + "baseLayerID": -1, + "baseLayerMD5": "62678324450bac2154703e2978e8ab61.svg", + "bitmapResolution": 1, + "rotationCenterX": 35, + "rotationCenterY": 39 + } + ], + "currentCostumeIndex": 0, + "scratchX": 12, + "scratchY": -131, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, { "name": "Witch", "md5": "cbc54e15cd62f0c16369587377636099.svg", @@ -15381,12 +15659,12 @@ "objName": "Witch", "sounds": [ { - "soundName": "pop", + "soundName": "Magic Spell", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1cb60ecdb1075c8769cb346d5c2a22c7.wav", + "sampleCount": 43077, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -15455,12 +15733,12 @@ "objName": "Wizard", "sounds": [ { - "soundName": "pop", + "soundName": "Magic Spell", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1cb60ecdb1075c8769cb346d5c2a22c7.wav", + "sampleCount": 43077, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -15500,12 +15778,60 @@ "spriteInfo": {} } }, + { + "name": "Wizard Hat", + "md5": "2da561a7e5cbfcda613f4750020d4aa5.svg", + "type": "sprite", + "tags": [ + "fashion", + "fantasy", + "winter" + ], + "info": [ + 0, + 1, + 1 + ], + "json": { + "objName": "Wizard Hat", + "sounds": [ + { + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + } + ], + "costumes": [ + { + "costumeName": "Wizard Hat", + "baseLayerID": -1, + "baseLayerMD5": "2da561a7e5cbfcda613f4750020d4aa5.svg", + "bitmapResolution": 1, + "rotationCenterX": 34, + "rotationCenterY": 60 + } + ], + "currentCostumeIndex": 0, + "scratchX": 3, + "scratchY": -34, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "visible": true, + "spriteInfo": {} + } + }, { "name": "Wizard-toad", "md5": "fd5c4cce36e866489febc227e23b21aa.svg", "type": "sprite", "tags": [ "fantasy", + "animals", "people", "ipzy", "castle", @@ -15515,18 +15841,26 @@ "info": [ 0, 2, - 1 + 2 ], "json": { "objName": "Wizard-toad", "sounds": [ { - "soundName": "pop", + "soundName": "Magic Spell", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" + "md5": "1cb60ecdb1075c8769cb346d5c2a22c7.wav", + "sampleCount": 43077, + "rate": 22050, + "format": "adpcm" + }, + { + "soundName": "croak", + "soundID": -1, + "md5": "c6ce0aadb89903a43f76fc20ea57633e.wav", + "sampleCount": 6424, + "rate": 22050, + "format": "adpcm" } ], "costumes": [ @@ -15560,26 +15894,27 @@ }, { "name": "Zebra", - "md5": "d8392aea6e767a4b5032cffd04e688e3.svg", + "md5": "f58632e6b34fa8f9b35219e52ed2c864.svg", "type": "sprite", "tags": [ "animals", + "zebra", "savanna", "robert hunter" ], "info": [ 0, - 3, + 2, 1 ], "json": { "objName": "Zebra", "sounds": [ { - "soundName": "pop", + "soundName": "horse gallop", "soundID": -1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, + "md5": "058a34b5fb8b57178b5322d994b6b8c8.wav", + "sampleCount": 38336, "rate": 11025, "format": "" } @@ -15588,7 +15923,7 @@ { "costumeName": "zebra-a", "baseLayerID": -1, - "baseLayerMD5": "d8392aea6e767a4b5032cffd04e688e3.svg", + "baseLayerMD5": "f58632e6b34fa8f9b35219e52ed2c864.svg", "bitmapResolution": 1, "rotationCenterX": 97, "rotationCenterY": 56 @@ -15596,17 +15931,9 @@ { "costumeName": "zebra-b", "baseLayerID": -1, - "baseLayerMD5": "3a6be1df1d9d9865b6bba4f569de6793.svg", + "baseLayerMD5": "ea23797598a55938a2d46f2b0a389fd6.svg", "bitmapResolution": 1, - "rotationCenterX": 97, - "rotationCenterY": 56 - }, - { - "costumeName": "zebra-c", - "baseLayerID": -1, - "baseLayerMD5": "b0d2d3e746493640fc34fd7b2efd6612.svg", - "bitmapResolution": 1, - "rotationCenterX": 97, + "rotationCenterX": 96, "rotationCenterY": 56 } ], @@ -18479,7 +18806,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -18541,7 +18868,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -18603,7 +18930,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -18665,7 +18992,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -18727,7 +19054,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -18789,7 +19116,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -18851,7 +19178,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -18913,7 +19240,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -18975,7 +19302,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -19037,7 +19364,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -19099,7 +19426,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -19161,7 +19488,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -19223,7 +19550,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -19285,7 +19612,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -19347,7 +19674,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -19409,7 +19736,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -19471,7 +19798,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -19533,7 +19860,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -19595,7 +19922,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -19657,7 +19984,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -19719,7 +20046,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -19781,7 +20108,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -19843,7 +20170,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -19905,7 +20232,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -19967,7 +20294,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, @@ -20029,7 +20356,7 @@ "type": "sprite", "tags": [ "alphabet", - "letter" + "letters" ], "info": [ 0, diff --git a/src/lib/libraries/tag-messages.js b/src/lib/libraries/tag-messages.js index 144e04a94..30079af52 100644 --- a/src/lib/libraries/tag-messages.js +++ b/src/lib/libraries/tag-messages.js @@ -120,5 +120,10 @@ export default defineMessages({ defaultMessage: 'Stories', description: 'Tag for filtering a library for stories', id: 'gui.libraryTags.stories' + }, + letters: { + defaultMessage: 'Letters', + description: 'Tag for filtering a library for letters', + id: 'gui.libraryTags.letters' } }); diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js index 936859666..4217b6e6b 100644 --- a/src/lib/make-toolbox-xml.js +++ b/src/lib/make-toolbox-xml.js @@ -138,7 +138,7 @@ const motion = function (isStage, targetId) { `; }; -const looks = function (isStage, targetId) { +const looks = function (isStage, targetId, costumeName, backdropName) { const hello = ScratchBlocks.ScratchMsgs.translate('LOOKS_HELLO', 'Hello!'); const hmm = ScratchBlocks.ScratchMsgs.translate('LOOKS_HMM', 'Hmm...'); return ` @@ -187,25 +187,33 @@ const looks = function (isStage, targetId) { ${isStage ? ` - + + ${backdropName} + - + + ${backdropName} + ` : ` - + + ${costumeName} + - + + ${backdropName} + @@ -267,17 +275,21 @@ const looks = function (isStage, targetId) { `; }; -const sound = function (isStage, targetId) { +const sound = function (isStage, targetId, soundName) { return ` - + + ${soundName} + - + + ${soundName} + @@ -692,17 +704,21 @@ const xmlClose = ''; /** * @param {!boolean} isStage - Whether the toolbox is for a stage-type target. * @param {?string} targetId - The current editing target - * @param {string?} categoriesXML - null for default toolbox, or an XML string with elements. + * @param {?string} categoriesXML - null for default toolbox, or an XML string with elements. + * @param {?string} costumeName - The name of the default selected costume dropdown. + * @param {?string} backdropName - The name of the default selected backdrop dropdown. + * @param {?string} soundName - The name of the default selected sound dropdown. * @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox. */ -const makeToolboxXML = function (isStage, targetId, categoriesXML) { +const makeToolboxXML = function (isStage, targetId, categoriesXML, + costumeName = '', backdropName = '', soundName = '') { const gap = [categorySeparator]; const everything = [ xmlOpen, motion(isStage, targetId), gap, - looks(isStage, targetId), gap, - sound(isStage, targetId), gap, + looks(isStage, targetId, costumeName, backdropName), gap, + sound(isStage, targetId, soundName), gap, events(isStage, targetId), gap, control(isStage, targetId), gap, sensing(isStage, targetId), gap, diff --git a/src/lib/opcode-labels.js b/src/lib/opcode-labels.js index 8f9fe82cb..dde8636be 100644 --- a/src/lib/opcode-labels.js +++ b/src/lib/opcode-labels.js @@ -89,8 +89,8 @@ const messages = defineMessages({ id: 'gui.opcodeLabels.date' }, sensing_current_dayofweek: { - defaultMessage: 'dayofweek', - description: 'Label for the current dayofweek monitor when shown on the stage', + defaultMessage: 'day of week', + description: 'Label for the current day of week monitor when shown on the stage', id: 'gui.opcodeLabels.dayofweek' }, sensing_current_hour: { @@ -210,7 +210,7 @@ class OpcodeLabels { this._opcodeMap.sensing_loudness.labelFn = () => this._translator(messages.sensing_loudness); this._opcodeMap.sensing_username.labelFn = () => this._translator(messages.sensing_username); this._opcodeMap.sensing_current.labelFn = params => { - switch (params.CURRENTMENU) { + switch (params.CURRENTMENU.toLowerCase()) { case 'year': return this._translator(messages.sensing_current_year); case 'month': @@ -238,7 +238,7 @@ class OpcodeLabels { getLabel (opcode) { if (opcode in this._opcodeMap) return this._opcodeMap[opcode]; return { - category: 'data', + category: 'extension', label: opcode }; } diff --git a/src/lib/project-fetcher-hoc.jsx b/src/lib/project-fetcher-hoc.jsx index ca847fe3f..888e378f5 100644 --- a/src/lib/project-fetcher-hoc.jsx +++ b/src/lib/project-fetcher-hoc.jsx @@ -4,16 +4,22 @@ import {intlShape, injectIntl} from 'react-intl'; import bindAll from 'lodash.bindall'; import {connect} from 'react-redux'; +import {setProjectUnchanged} from '../reducers/project-changed'; import { LoadingStates, - defaultProjectId, + getIsCreatingNew, getIsFetchingWithId, + getIsLoading, + getIsShowingProject, onFetchedProjectData, projectError, setProjectId } from '../reducers/project-state'; +import { + activateTab, + BLOCKS_TAB_INDEX +} from '../reducers/editor-tab'; -import analytics from './analytics'; import log from './log'; import storage from './storage'; @@ -54,6 +60,12 @@ const ProjectFetcherHOC = function (WrappedComponent) { if (this.props.isFetchingWithId && !prevProps.isFetchingWithId) { this.fetchProject(this.props.reduxProjectId, this.props.loadingState); } + if (this.props.isShowingProject && !prevProps.isShowingProject) { + this.props.onProjectUnchanged(); + } + if (this.props.isShowingProject && (prevProps.isLoadingProject || prevProps.isCreatingNew)) { + this.props.onActivateTab(BLOCKS_TAB_INDEX); + } } fetchProject (projectId, loadingState) { return storage @@ -61,17 +73,10 @@ const ProjectFetcherHOC = function (WrappedComponent) { .then(projectAsset => { if (projectAsset) { this.props.onFetchedProjectData(projectAsset.data, loadingState); - } - }) - .then(() => { - if (projectId !== defaultProjectId) { - // if not default project, register a project load event - analytics.event({ - category: 'project', - action: 'Load Project', - label: projectId, - nonInteraction: true - }); + } else { + // Treat failure to load as an error + // Throw to be caught by catch later on + throw new Error('Could not find project'); } }) .catch(err => { @@ -84,9 +89,12 @@ const ProjectFetcherHOC = function (WrappedComponent) { /* eslint-disable no-unused-vars */ assetHost, intl, + isLoadingProject: isLoadingProjectProp, loadingState, + onActivateTab, onError: onErrorProp, onFetchedProjectData: onFetchedProjectDataProp, + onProjectUnchanged, projectHost, projectId, reduxProjectId, @@ -108,9 +116,12 @@ const ProjectFetcherHOC = function (WrappedComponent) { canSave: PropTypes.bool, intl: intlShape.isRequired, isFetchingWithId: PropTypes.bool, + isLoadingProject: PropTypes.bool, loadingState: PropTypes.oneOf(LoadingStates), + onActivateTab: PropTypes.func, onError: PropTypes.func, onFetchedProjectData: PropTypes.func, + onProjectUnchanged: PropTypes.func, projectHost: PropTypes.string, projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), reduxProjectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), @@ -122,15 +133,20 @@ const ProjectFetcherHOC = function (WrappedComponent) { }; const mapStateToProps = state => ({ + isCreatingNew: getIsCreatingNew(state.scratchGui.projectState.loadingState), isFetchingWithId: getIsFetchingWithId(state.scratchGui.projectState.loadingState), + isLoadingProject: getIsLoading(state.scratchGui.projectState.loadingState), + isShowingProject: getIsShowingProject(state.scratchGui.projectState.loadingState), loadingState: state.scratchGui.projectState.loadingState, reduxProjectId: state.scratchGui.projectState.projectId }); const mapDispatchToProps = dispatch => ({ + onActivateTab: tab => dispatch(activateTab(tab)), onError: error => dispatch(projectError(error)), onFetchedProjectData: (projectData, loadingState) => dispatch(onFetchedProjectData(projectData, loadingState)), - setProjectId: projectId => dispatch(setProjectId(projectId)) + setProjectId: projectId => dispatch(setProjectId(projectId)), + onProjectUnchanged: () => dispatch(setProjectUnchanged()) }); // Allow incoming props to override redux-provided props. Used to mock in tests. const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign( diff --git a/src/lib/project-saver-hoc.jsx b/src/lib/project-saver-hoc.jsx index 589dcaf7d..cc409dab1 100644 --- a/src/lib/project-saver-hoc.jsx +++ b/src/lib/project-saver-hoc.jsx @@ -1,22 +1,36 @@ +import bindAll from 'lodash.bindall'; import React from 'react'; import PropTypes from 'prop-types'; +import queryString from 'query-string'; import {connect} from 'react-redux'; import VM from 'scratch-vm'; +import xhr from 'xhr'; +import log from '../lib/log'; import storage from '../lib/storage'; +import dataURItoBlob from '../lib/data-uri-to-blob'; + +import { + showAlertWithTimeout, + showStandardAlert +} from '../reducers/alerts'; +import {setAutoSaveTimeoutId} from '../reducers/timeout'; +import {setProjectUnchanged} from '../reducers/project-changed'; import { LoadingStates, + autoUpdateProject, createProject, doneCreatingProject, doneUpdatingProject, + getIsAnyCreatingNewState, getIsCreatingCopy, getIsCreatingNew, + getIsManualUpdating, getIsRemixing, - getIsShowingProject, + getIsShowingWithId, getIsShowingWithoutId, getIsUpdating, - projectError, - updateProject + projectError } from '../reducers/project-state'; /** @@ -30,71 +44,153 @@ import { */ const ProjectSaverHOC = function (WrappedComponent) { class ProjectSaverComponent extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'leavePageConfirm', + 'tryToAutoSave' + ]); + } + componentWillMount () { + if (typeof window === 'object') { + // Note: it might be better to use a listener instead of assigning onbeforeunload; + // but then it'd be hard to turn this listening off in our tests + window.onbeforeunload = e => this.leavePageConfirm(e); + } + } componentDidUpdate (prevProps) { + if (this.props.projectChanged && !prevProps.projectChanged) { + this.scheduleAutoSave(); + } if (this.props.isUpdating && !prevProps.isUpdating) { - this.storeProject(this.props.reduxProjectId) - .then(() => { - // there is nothing we expect to find in response that we need to check here - this.props.onUpdatedProject(this.props.loadingState); - }) - .catch(err => { - // NOTE: should throw up a notice for user - this.props.onProjectError(`Saving the project failed with error: ${err}`); - }); + this.updateProjectToStorage(); } - // TODO: distinguish between creating new, remixing, and saving as a copy if (this.props.isCreatingNew && !prevProps.isCreatingNew) { - this.storeProject(null) - .then(response => { - this.props.onCreatedProject(response.id.toString(), this.props.loadingState); - }) - .catch(err => { - this.props.onProjectError(`Creating a new project failed with error: ${err}`); - }); + this.createNewProjectToStorage(); } if (this.props.isCreatingCopy && !prevProps.isCreatingCopy) { - this.storeProject(null, { - original_id: this.props.reduxProjectId, - is_copy: 1, - title: this.props.reduxProjectTitle - }) - .then(response => { - this.props.onCreatedProject(response.id.toString(), this.props.loadingState); - }) - .catch(err => { - this.props.onProjectError(`Creating a project copy failed with error: ${err}`); - }); + this.createCopyToStorage(); } if (this.props.isRemixing && !prevProps.isRemixing) { - this.storeProject(null, { - original_id: this.props.reduxProjectId, - is_remix: 1, - title: this.props.reduxProjectTitle - }) - .then(response => { - this.props.onCreatedProject(response.id.toString(), this.props.loadingState); - }) - .catch(err => { - this.props.onProjectError(`Remixing a project failed with error: ${err}`); - }); + this.props.onRemixing(true); + this.createRemixToStorage(); + } else if (!this.props.isRemixing && prevProps.isRemixing) { + this.props.onRemixing(false); } - // check if the project state, and user capabilities, have changed so as to indicate - // that we should create or update project + // see if we should "create" the current project on the server // - // if we're newly able to create this project on the server, create it! - const showingCreateable = this.props.canCreateNew && this.props.isShowingWithoutId; - const prevShowingCreateable = prevProps.canCreateNew && prevProps.isShowingWithoutId; - if (showingCreateable && !prevShowingCreateable) { + // don't try to create or save immediately after trying to create + if (prevProps.isCreatingNew) return; + // if we're newly able to create this project, create it! + if (this.isShowingCreatable(this.props) && !this.isShowingCreatable(prevProps)) { this.props.onCreateProject(); - } else { - // if we're newly able to save this project, save it! - const showingSaveable = this.props.canSave && this.props.isShowingWithId; - const becameAbleToSave = this.props.canSave && !prevProps.canSave; - if (showingSaveable && becameAbleToSave) { - this.props.onUpdateProject(); - } } + + // see if we should save/update the current project on the server + // + // don't try to save immediately after trying to save + if (prevProps.isUpdating) return; + // if we're newly able to save this project, save it! + const becameAbleToSave = this.props.canSave && !prevProps.canSave; + const becameShared = this.props.isShared && !prevProps.isShared; + if (this.props.isShowingSaveable && (becameAbleToSave || becameShared)) { + this.props.onAutoUpdateProject(); + } + } + componentWillUnmount () { + this.clearAutoSaveTimeout(); + // Cant unset the beforeunload because it might no longer belong to this component + // i.e. if another of this component has been mounted before this one gets unmounted + // which happens when going from project to editor view. + // window.onbeforeunload = undefined; // eslint-disable-line no-undefined + } + leavePageConfirm (e) { + if (this.props.projectChanged) { + // both methods of returning a value may be necessary for browser compatibility + (e || window.event).returnValue = true; + return true; + } + return; // Returning undefined prevents the prompt from coming up + } + clearAutoSaveTimeout () { + if (this.props.autoSaveTimeoutId !== null) { + clearTimeout(this.props.autoSaveTimeoutId); + this.props.setAutoSaveTimeoutId(null); + } + } + scheduleAutoSave () { + if (this.props.isShowingSaveable && this.props.autoSaveTimeoutId === null) { + const timeoutId = setTimeout(this.tryToAutoSave, + this.props.autoSaveIntervalSecs * 1000); + this.props.setAutoSaveTimeoutId(timeoutId); + } + } + tryToAutoSave () { + if (this.props.projectChanged && this.props.isShowingSaveable) { + this.props.onAutoUpdateProject(); + } + } + isShowingCreatable (props) { + return props.canCreateNew && props.isShowingWithoutId; + } + updateProjectToStorage () { + this.props.onShowSavingAlert(); + return this.storeProject(this.props.reduxProjectId) + .then(() => { + // there's an http response object available here, but we don't need to examine + // it, because there are no values contained in it that we care about + this.props.onUpdatedProject(this.props.loadingState); + this.props.onShowSaveSuccessAlert(); + }) + .catch(err => { + // Always show the savingError alert because it gives the + // user the chance to download or retry the save manually. + this.props.onShowAlert('savingError'); + this.props.onProjectError(err); + }); + } + createNewProjectToStorage () { + return this.storeProject(null) + .then(response => { + this.props.onCreatedProject(response.id.toString(), this.props.loadingState); + }) + .catch(err => { + this.props.onShowAlert('creatingError'); + this.props.onProjectError(err); + }); + } + createCopyToStorage () { + this.props.onShowCreatingCopyAlert(); + return this.storeProject(null, { + original_id: this.props.reduxProjectId, + is_copy: 1, + title: this.props.reduxProjectTitle + }) + .then(response => { + this.props.onCreatedProject(response.id.toString(), this.props.loadingState); + this.props.onShowCopySuccessAlert(); + }) + .catch(err => { + this.props.onShowAlert('creatingError'); + this.props.onProjectError(err); + }); + } + createRemixToStorage () { + this.props.onShowCreatingRemixAlert(); + return this.storeProject(null, { + original_id: this.props.reduxProjectId, + is_remix: 1, + title: this.props.reduxProjectTitle + }) + .then(response => { + this.props.onCreatedProject(response.id.toString(), this.props.loadingState); + this.props.onShowRemixSuccessAlert(); + }) + .catch(err => { + this.props.onShowAlert('creatingError'); + this.props.onProjectError(err); + }); } /** * storeProject: @@ -103,94 +199,223 @@ const ProjectSaverHOC = function (WrappedComponent) { * @param {?object} requestParams - object of params to add to request body */ storeProject (projectId, requestParams) { - return this.props.vm.saveProjectSb3() - .then(content => { - const assetType = storage.AssetType.Project; - const dataFormat = storage.DataFormat.SB3; - const body = new FormData(); - body.append('sb3_file', content, 'sb3_file'); - if (requestParams) { - for (const key in requestParams) { - body.append(key, requestParams[key]); + requestParams = requestParams || {}; + this.clearAutoSaveTimeout(); + // Serialize VM state now before embarking on + // the asynchronous journey of storing assets to + // the server. This ensures that assets don't update + // while in the process of saving a project (e.g. the + // serialized project refers to a newer asset than what + // we just finished saving). + const savedVMState = this.props.vm.toJSON(); + return Promise.all(this.props.vm.assets + .filter(asset => !asset.clean) + .map( + asset => storage.store( + asset.assetType, + asset.dataFormat, + asset.data, + asset.assetId + ).then( + () => (asset.clean = true) + ) + ) + ).then(() => { + const opts = { + body: savedVMState, + // If we set json:true then the body is double-stringified, so don't + headers: { + 'Content-Type': 'application/json' + }, + withCredentials: true + }; + const creatingProject = projectId === null || typeof projectId === 'undefined'; + let qs = queryString.stringify(requestParams); + if (qs) qs = `?${qs}`; + if (creatingProject) { + Object.assign(opts, { + method: 'post', + url: `${storage.projectHost}/${qs}` + }); + } else { + Object.assign(opts, { + method: 'put', + url: `${storage.projectHost}/${projectId}${qs}` + }); + } + return new Promise((resolve, reject) => { + xhr(opts, (err, response) => { + if (err) return reject(err); + let body; + try { + // Since we didn't set json: true, we have to parse manually + body = JSON.parse(response.body); + } catch (e) { + return reject(e); } + body.id = projectId; + if (creatingProject) { + body.id = body['content-name']; + } + resolve(body); + }); + }); + }) + .then(response => { + this.props.onSetProjectUnchanged(); + const id = response.id.toString(); + if (id && this.props.onUpdateProjectThumbnail) { + this.storeProjectThumbnail(id); } - // when id is undefined or null, storage.store as we have - // configured it will create a new project with id - return storage.store( - assetType, - dataFormat, - body, - projectId - ); + return response; + }) + .catch(err => { + log.error(err); + throw err; // pass the error up the chain }); } + + /** + * Store a snapshot of the project once it has been saved/created. + * Needs to happen _after_ save because the project must have an ID. + * @param {!string} projectId - id of the project, must be defined. + */ + storeProjectThumbnail (projectId) { + try { + this.props.vm.postIOData('video', {forceTransparentPreview: true}); + this.props.vm.renderer.requestSnapshot(dataURI => { + this.props.vm.postIOData('video', {forceTransparentPreview: false}); + this.props.onUpdateProjectThumbnail( + projectId, dataURItoBlob(dataURI)); + }); + this.props.vm.renderer.draw(); + } catch (e) { + log.error('Project thumbnail save error', e); + // This is intentionally fire/forget because a failure + // to save the thumbnail is not vitally important to the user. + } + } + render () { const { /* eslint-disable no-unused-vars */ - isCreatingCopy: isCreatingCopyProp, - isCreatingNew: isCreatingNewProp, - isRemixing: isRemixingProp, - isShowingWithId: isShowingWithIdProp, - isShowingWithoutId: isShowingWithoutIdProp, - isUpdating: isUpdatingProp, + autoSaveTimeoutId, + autoSaveIntervalSecs, + isCreatingCopy, + isCreatingNew, + projectChanged, + isAnyCreatingNewState, + isManualUpdating, + isRemixing, + isShowingSaveable, + isShowingWithId, + isShowingWithoutId, + isUpdating, loadingState, - onCreatedProject: onCreatedProjectProp, - onCreateProject: onCreateProjectProp, - onProjectError: onProjectErrorProp, - onUpdatedProject: onUpdatedProjectProp, - onUpdateProject: onUpdateProjectProp, + onAutoUpdateProject, + onCreatedProject, + onCreateProject, + onProjectError, + onRemixing, + onSetProjectUnchanged, + onShowAlert, + onShowCopySuccessAlert, + onShowRemixSuccessAlert, + onShowCreatingCopyAlert, + onShowCreatingRemixAlert, + onShowSaveSuccessAlert, + onShowSavingAlert, + onUpdatedProject, + onUpdateProjectThumbnail, reduxProjectId, reduxProjectTitle, + setAutoSaveTimeoutId: setAutoSaveTimeoutIdProp, /* eslint-enable no-unused-vars */ ...componentProps } = this.props; return ( ); } } + ProjectSaverComponent.propTypes = { + autoSaveTimeoutId: PropTypes.number, canCreateNew: PropTypes.bool, canSave: PropTypes.bool, + isAnyCreatingNewState: PropTypes.bool, isCreatingCopy: PropTypes.bool, isCreatingNew: PropTypes.bool, + isManualUpdating: PropTypes.bool, isRemixing: PropTypes.bool, + isShared: PropTypes.bool, + isShowingSaveable: PropTypes.bool, isShowingWithId: PropTypes.bool, isShowingWithoutId: PropTypes.bool, isUpdating: PropTypes.bool, loadingState: PropTypes.oneOf(LoadingStates), + onAutoUpdateProject: PropTypes.func, onCreateProject: PropTypes.func, onCreatedProject: PropTypes.func, onProjectError: PropTypes.func, - onUpdateProject: PropTypes.func, + onRemixing: PropTypes.func, + onShowAlert: PropTypes.func, + onShowCopySuccessAlert: PropTypes.func, + onShowCreatingCopyAlert: PropTypes.func, + onShowCreatingRemixAlert: PropTypes.func, + onShowRemixSuccessAlert: PropTypes.func, + onShowSaveSuccessAlert: PropTypes.func, + onShowSavingAlert: PropTypes.func, + onUpdateProjectThumbnail: PropTypes.func, onUpdatedProject: PropTypes.func, + projectChanged: PropTypes.bool, reduxProjectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), reduxProjectTitle: PropTypes.string, vm: PropTypes.instanceOf(VM).isRequired }; - const mapStateToProps = state => { + ProjectSaverComponent.defaultProps = { + autoSaveIntervalSecs: 120, + onRemixing: () => {} + }; + const mapStateToProps = (state, ownProps) => { const loadingState = state.scratchGui.projectState.loadingState; + const isShowingWithId = getIsShowingWithId(loadingState); return { + autoSaveTimeoutId: state.scratchGui.timeout.autoSaveTimeoutId, + isAnyCreatingNewState: getIsAnyCreatingNewState(loadingState), isCreatingCopy: getIsCreatingCopy(loadingState), isCreatingNew: getIsCreatingNew(loadingState), isRemixing: getIsRemixing(loadingState), - isShowingWithId: getIsShowingProject(loadingState), + isShowingSaveable: ownProps.canSave && isShowingWithId, + isShowingWithId: isShowingWithId, isShowingWithoutId: getIsShowingWithoutId(loadingState), isUpdating: getIsUpdating(loadingState), + isManualUpdating: getIsManualUpdating(loadingState), loadingState: loadingState, + projectChanged: state.scratchGui.projectChanged, reduxProjectId: state.scratchGui.projectState.projectId, reduxProjectTitle: state.scratchGui.projectTitle, vm: state.scratchGui.vm }; }; const mapDispatchToProps = dispatch => ({ + onAutoUpdateProject: () => dispatch(autoUpdateProject()), onCreatedProject: (projectId, loadingState) => dispatch(doneCreatingProject(projectId, loadingState)), onCreateProject: () => dispatch(createProject()), onProjectError: error => dispatch(projectError(error)), - onUpdateProject: () => dispatch(updateProject()), - onUpdatedProject: (projectId, loadingState) => dispatch(doneUpdatingProject(projectId, loadingState)) + onSetProjectUnchanged: () => dispatch(setProjectUnchanged()), + onShowAlert: alertType => dispatch(showStandardAlert(alertType)), + onShowCopySuccessAlert: () => showAlertWithTimeout(dispatch, 'createCopySuccess'), + onShowRemixSuccessAlert: () => showAlertWithTimeout(dispatch, 'createRemixSuccess'), + onShowCreatingCopyAlert: () => showAlertWithTimeout(dispatch, 'creatingCopy'), + onShowCreatingRemixAlert: () => showAlertWithTimeout(dispatch, 'creatingRemix'), + onShowSaveSuccessAlert: () => showAlertWithTimeout(dispatch, 'saveSuccess'), + onShowSavingAlert: () => showAlertWithTimeout(dispatch, 'saving'), + onUpdatedProject: (projectId, loadingState) => dispatch(doneUpdatingProject(projectId, loadingState)), + setAutoSaveTimeoutId: id => dispatch(setAutoSaveTimeoutId(id)) }); // Allow incoming props to override redux-provided props. Used to mock in tests. const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign( diff --git a/src/lib/query-parser-hoc.jsx b/src/lib/query-parser-hoc.jsx new file mode 100644 index 000000000..161b1a34a --- /dev/null +++ b/src/lib/query-parser-hoc.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import queryString from 'query-string'; +import {connect} from 'react-redux'; + +import {detectTutorialId} from './tutorial-from-url'; + +import {activateDeck} from '../reducers/cards'; +import {openTipsLibrary, closePreviewInfo} from '../reducers/modals'; + +/* Higher Order Component to get parameters from the URL query string and initialize redux state + * @param {React.Component} WrappedComponent: component to render + * @returns {React.Component} component with query parsing behavior + */ +const QueryParserHOC = function (WrappedComponent) { + class QueryParserComponent extends React.Component { + constructor (props) { + super(props); + const queryParams = queryString.parse(location.search); + const tutorialId = detectTutorialId(queryParams); + if (tutorialId) { + if (tutorialId === 'all') { + this.openTutorials(); + } else { + this.setActiveCards(tutorialId); + } + } + } + setActiveCards (tutorialId) { + this.props.onUpdateReduxDeck(tutorialId); + } + openTutorials () { + this.props.onOpenTipsLibrary(); + } + render () { + const { + onOpenTipsLibrary, // eslint-disable-line no-unused-vars + onUpdateReduxDeck, // eslint-disable-line no-unused-vars + ...componentProps + } = this.props; + return ( + + ); + } + } + QueryParserComponent.propTypes = { + onOpenTipsLibrary: PropTypes.func, + onUpdateReduxDeck: PropTypes.func + }; + const mapDispatchToProps = dispatch => ({ + onOpenTipsLibrary: () => { + dispatch(openTipsLibrary()); + dispatch(closePreviewInfo()); + }, + onUpdateReduxDeck: tutorialId => { + dispatch(activateDeck(tutorialId)); + dispatch(closePreviewInfo()); + } + }); + return connect( + null, + mapDispatchToProps + )(QueryParserComponent); +}; + +export { + QueryParserHOC as default +}; diff --git a/src/lib/randomize-sprite-position.js b/src/lib/randomize-sprite-position.js new file mode 100644 index 000000000..bd4c55eb9 --- /dev/null +++ b/src/lib/randomize-sprite-position.js @@ -0,0 +1,16 @@ +const randomizeSpritePosition = spriteObject => { + // https://github.com/LLK/scratch-flash/blob/689f3c79a7e8b2e98f5be80056d877f303a8d8ba/src/Scratch.as#L1385 + const randomX = Math.floor((200 * Math.random()) - 100); + const randomY = Math.floor((100 * Math.random()) - 50); + if (spriteObject.hasOwnProperty('json')) { + // Library sprite object + spriteObject.json.scratchX = randomX; + spriteObject.json.scratchY = randomY; + } else if (spriteObject.hasOwnProperty('x') && spriteObject.hasOwnProperty('y')) { + // Scratch 3 sprite object + spriteObject.x = randomX; + spriteObject.y = randomY; + } +}; + +export default randomizeSpritePosition; diff --git a/src/lib/screen-utils.js b/src/lib/screen-utils.js index d61201784..a94a67d5b 100644 --- a/src/lib/screen-utils.js +++ b/src/lib/screen-utils.js @@ -69,6 +69,10 @@ const getStageDimensions = (stageSize, isFullScreen) => { stageDimensions.width = stageDimensions.scale * stageDimensions.widthDefault; } + // Round off dimensions to prevent resampling/blurriness + stageDimensions.height = Math.round(stageDimensions.height); + stageDimensions.width = Math.round(stageDimensions.width); + return stageDimensions; }; diff --git a/src/lib/sortable-hoc.jsx b/src/lib/sortable-hoc.jsx index d3f9c59e1..eb971d0c0 100644 --- a/src/lib/sortable-hoc.jsx +++ b/src/lib/sortable-hoc.jsx @@ -68,16 +68,22 @@ const SortableHOC = function (WrappedComponent) { } return ordering; } + getMouseOverIndex () { // MouseOverIndex is the index that the current drag wants to place the // the dragging object. Obviously only exists if there is a drag (i.e. currentOffset). + // Return null if outside the container, zero if there are no boxes. let mouseOverIndex = null; if (this.props.dragInfo.currentOffset) { const {x, y} = this.props.dragInfo.currentOffset; const {top, left, bottom, right} = this.containerBox; if (x >= left && x <= right && y >= top && y <= bottom) { - mouseOverIndex = indexForPositionOnList( - this.props.dragInfo.currentOffset, this.boxes); + if (this.boxes.length === 0) { + mouseOverIndex = 0; + } else { + mouseOverIndex = indexForPositionOnList( + this.props.dragInfo.currentOffset, this.boxes); + } } } return mouseOverIndex; diff --git a/src/lib/storage.js b/src/lib/storage.js index 59c0ecc6b..a3f240685 100644 --- a/src/lib/storage.js +++ b/src/lib/storage.js @@ -20,7 +20,12 @@ class Storage extends ScratchStorage { ); this.addWebStore( [this.AssetType.ImageVector, this.AssetType.ImageBitmap, this.AssetType.Sound], - this.getAssetGetConfig.bind(this) + this.getAssetGetConfig.bind(this), + // We set both the create and update configs to the same method because + // storage assumes it should update if there is an assetId, but the + // asset store uses the assetId as part of the create URI. + this.getAssetCreateConfig.bind(this), + this.getAssetCreateConfig.bind(this) ); this.addWebStore( [this.AssetType.Sound], @@ -31,7 +36,7 @@ class Storage extends ScratchStorage { this.projectHost = projectHost; } getProjectGetConfig (projectAsset) { - return `${this.projectHost}/internalapi/project/${projectAsset.assetId}/get/`; + return `${this.projectHost}/${projectAsset.assetId}`; } getProjectCreateConfig () { return { @@ -51,6 +56,17 @@ class Storage extends ScratchStorage { getAssetGetConfig (asset) { return `${this.assetHost}/internalapi/asset/${asset.assetId}.${asset.dataFormat}/get/`; } + getAssetCreateConfig (asset) { + return { + // There is no such thing as updating assets, but storage assumes it + // should update if there is an assetId, and the asset store uses the + // assetId as part of the create URI. So, force the method to POST. + // Then when storage finds this config to use for the "update", still POSTs + method: 'post', + url: `${this.assetHost}/${asset.assetId}.${asset.dataFormat}`, + withCredentials: true + }; + } setTranslatorFunction (translator) { this.translator = translator; this.cacheDefaultProject(); diff --git a/src/lib/supported-browser.js b/src/lib/supported-browser.js index caeac5fe3..6f5ac8bac 100644 --- a/src/lib/supported-browser.js +++ b/src/lib/supported-browser.js @@ -1,16 +1,34 @@ import bowser from 'bowser'; +const minVersions = { + chrome: '63', + msedge: '15', + firefox: '57', + safari: '11' +}; + /** - * Helper function to determine if the browser is supported. + * Helper function to determine if the browser is supported at all. * @returns {boolean} False if the platform is definitely not supported. */ -export default function () { - if (bowser.msie || - bowser.vivaldi || - bowser.opera || - bowser.silk) { +const supportedBrowser = () => { + if (bowser.msie) { return false; } - // @todo Should also test for versions of supported browsers return true; -} +}; + +/** + * Helper function to determine if the browser meets the minimum recommended version + * @returns {boolean} False if the browser isn't a recommended browser, or doesn't + * meet the minimum version for recommended browsers. + * NOTE: uses strict_mode==true so that any browser not listed in the minVersions + * always returns false + */ + +const recommendedBrowser = () => !bowser.isUnsupportedBrowser(minVersions, true); + +export { + supportedBrowser as default, + recommendedBrowser +}; diff --git a/src/lib/throttled-property-hoc.jsx b/src/lib/throttled-property-hoc.jsx new file mode 100644 index 000000000..7e508dfbe --- /dev/null +++ b/src/lib/throttled-property-hoc.jsx @@ -0,0 +1,49 @@ +import React from 'react'; + +/* Higher Order Component to throttle updates to specific props. + * Why? Because certain prop updates are expensive, and need to be throttled. + * This allows renders when other properties change, and will use the last + * rendered value of a prop for comparison. + * @param {string} propName the name of the prop to throttle updates from. + * @param {string} throttleTime the minimum time between updates to that specific property. + * @returns {function} a function that accepts a component to wrap. + */ +const ThrottledPropertyHOC = function (propName, throttleTime) { + /** + * The function to be called with a React component to wrap it. + * @param {React.Component} WrappedComponent - Component to wrap with throttler. + * @returns {React.Component} the component wrapped with the throttler. + */ + return function (WrappedComponent) { + class ThrottledPropertyWrapper extends React.Component { + shouldComponentUpdate (nextProps) { + for (const property in nextProps) { + if (property !== propName && this.props[property] !== nextProps[property]) { + return true; // Always update if another property has changed + } + } + + // If only that prop has changed, allow update to go to render based + // on _lastRenderedTime and _lastRenderTime are updated in render + if (nextProps[propName] !== this._lastRenderedValue && + Date.now() - this._lastRenderTime > throttleTime + ) { + return true; // Allow this update to go to render + } + + return false; + } + render () { + this._lastRenderTime = Date.now(); + this._lastRenderedValue = this.props[propName]; + return ( + + ); + } + } + + return ThrottledPropertyWrapper; + }; +}; + +export default ThrottledPropertyHOC; diff --git a/src/lib/titled-hoc.jsx b/src/lib/titled-hoc.jsx index fea6a9388..377d6150d 100644 --- a/src/lib/titled-hoc.jsx +++ b/src/lib/titled-hoc.jsx @@ -22,6 +22,7 @@ const TitledHOC = function (WrappedComponent) { render () { return ( { /** * Check if there's a tutorial id provided as a query parameter in the URL. * Return the corresponding tutorial id or null if not found. + * @param {object} queryParams the results of parsing the query string * @return {string} The ID of the requested tutorial or null if no tutorial was * requested or found. */ -const detectTutorialId = () => { - const queryParams = queryString.parse(location.search); +const detectTutorialId = queryParams => { const tutorialID = Array.isArray(queryParams.tutorial) ? queryParams.tutorial[0] : queryParams.tutorial; diff --git a/src/lib/variable-utils.js b/src/lib/variable-utils.js index 12bcf4704..10df7df25 100644 --- a/src/lib/variable-utils.js +++ b/src/lib/variable-utils.js @@ -19,6 +19,7 @@ const setVariableValue = (vm, targetId, variableId, value) => { }; export { + getVariable, getVariableValue, setVariableValue }; diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx index d130cfcdd..efeaa3500 100644 --- a/src/lib/vm-listener-hoc.jsx +++ b/src/lib/vm-listener-hoc.jsx @@ -8,8 +8,9 @@ import {connect} from 'react-redux'; import {updateTargets} from '../reducers/targets'; import {updateBlockDrag} from '../reducers/block-drag'; import {updateMonitors} from '../reducers/monitors'; -import {setRunningState, setTurboState} from '../reducers/vm-status'; -import {showAlert} from '../reducers/alerts'; +import {setProjectChanged, setProjectUnchanged} from '../reducers/project-changed'; +import {setRunningState, setTurboState, setStartedState} from '../reducers/vm-status'; +import {showExtensionAlert} from '../reducers/alerts'; import {updateMicIndicator} from '../reducers/mic-indicator'; /* @@ -24,6 +25,7 @@ const vmListenerHOC = function (WrappedComponent) { bindAll(this, [ 'handleKeyDown', 'handleKeyUp', + 'handleProjectChanged', 'handleTargetsUpdate' ]); // We have to start listening to the vm here rather than in @@ -39,7 +41,10 @@ const vmListenerHOC = function (WrappedComponent) { this.props.vm.on('TURBO_MODE_OFF', this.props.onTurboModeOff); this.props.vm.on('PROJECT_RUN_START', this.props.onProjectRunStart); this.props.vm.on('PROJECT_RUN_STOP', this.props.onProjectRunStop); - this.props.vm.on('PERIPHERAL_DISCONNECT_ERROR', this.props.onShowAlert); + this.props.vm.on('PROJECT_CHANGED', this.handleProjectChanged); + this.props.vm.on('RUNTIME_STARTED', this.props.onRuntimeStarted); + this.props.vm.on('PROJECT_START', this.props.onGreenFlag); + this.props.vm.on('PERIPHERAL_CONNECTION_LOST_ERROR', this.props.onShowExtensionAlert); this.props.vm.on('MIC_LISTENING', this.props.onMicListeningUpdate); } @@ -58,18 +63,23 @@ const vmListenerHOC = function (WrappedComponent) { // Re-request a targets update when the shouldEmitTargetsUpdate state changes to true // i.e. when the editor transitions out of fullscreen/player only modes if (this.props.shouldEmitTargetsUpdate && !prevProps.shouldEmitTargetsUpdate) { - this.props.vm.emitTargetsUpdate(); + this.props.vm.emitTargetsUpdate(false /* Emit the event, but do not trigger project change */); } } componentWillUnmount () { - this.props.vm.removeListener('PERIPHERAL_DISCONNECT_ERROR', this.props.onShowAlert); + this.props.vm.removeListener('PERIPHERAL_CONNECTION_LOST_ERROR', this.props.onShowExtensionAlert); if (this.props.attachKeyboardEvents) { document.removeEventListener('keydown', this.handleKeyDown); document.removeEventListener('keyup', this.handleKeyUp); } } + handleProjectChanged () { + if (this.props.shouldEmitUpdates) { + this.props.onProjectChanged(); + } + } handleTargetsUpdate (data) { - if (this.props.shouldEmitTargetsUpdate) { + if (this.props.shouldEmitUpdates) { this.props.onTargetsUpdate(data); } } @@ -82,6 +92,12 @@ const vmListenerHOC = function (WrappedComponent) { key: e.key, isDown: true }); + + // Prevent space/arrow key from scrolling the page. + if (e.keyCode === 32 || // 32=space + (e.keyCode >= 37 && e.keyCode <= 40)) { // 37, 38, 39, 40 are arrows + e.preventDefault(); + } } handleKeyUp (e) { // Always capture up events, @@ -101,18 +117,22 @@ const vmListenerHOC = function (WrappedComponent) { const { /* eslint-disable no-unused-vars */ attachKeyboardEvents, - shouldEmitTargetsUpdate, + shouldEmitUpdates, onBlockDragUpdate, + onGreenFlag, onKeyDown, onKeyUp, onMicListeningUpdate, onMonitorsUpdate, onTargetsUpdate, + onProjectChanged, onProjectRunStart, onProjectRunStop, + onProjectSaved, + onRuntimeStarted, onTurboModeOff, onTurboModeOn, - onShowAlert, + onShowExtensionAlert, /* eslint-enable no-unused-vars */ ...props } = this.props; @@ -122,26 +142,31 @@ const vmListenerHOC = function (WrappedComponent) { VMListener.propTypes = { attachKeyboardEvents: PropTypes.bool, onBlockDragUpdate: PropTypes.func.isRequired, + onGreenFlag: PropTypes.func, onKeyDown: PropTypes.func, onKeyUp: PropTypes.func, onMicListeningUpdate: PropTypes.func.isRequired, onMonitorsUpdate: PropTypes.func.isRequired, onProjectRunStart: PropTypes.func.isRequired, onProjectRunStop: PropTypes.func.isRequired, - onShowAlert: PropTypes.func.isRequired, + onRuntimeStarted: PropTypes.func.isRequired, + onShowExtensionAlert: PropTypes.func.isRequired, onTargetsUpdate: PropTypes.func.isRequired, onTurboModeOff: PropTypes.func.isRequired, onTurboModeOn: PropTypes.func.isRequired, - shouldEmitTargetsUpdate: PropTypes.bool, + shouldEmitUpdates: PropTypes.bool, username: PropTypes.string, vm: PropTypes.instanceOf(VM).isRequired }; VMListener.defaultProps = { - attachKeyboardEvents: true + attachKeyboardEvents: true, + onGreenFlag: () => ({}) }; const mapStateToProps = state => ({ - // Do not emit target updates in fullscreen or player only mode - shouldEmitTargetsUpdate: !state.scratchGui.mode.isFullScreen && !state.scratchGui.mode.isPlayerOnly, + // Do not emit target or project updates in fullscreen or player only mode + // or when recording sounds (it leads to garbled recordings on low-power machines) + shouldEmitUpdates: !state.scratchGui.mode.isFullScreen && !state.scratchGui.mode.isPlayerOnly && + !state.scratchGui.modals.soundRecorder, vm: state.scratchGui.vm, username: state.session && state.session.session && state.session.session.user ? state.session.session.user.username : '' @@ -158,10 +183,13 @@ const vmListenerHOC = function (WrappedComponent) { }, onProjectRunStart: () => dispatch(setRunningState(true)), onProjectRunStop: () => dispatch(setRunningState(false)), + onProjectChanged: () => dispatch(setProjectChanged()), + onProjectSaved: () => dispatch(setProjectUnchanged()), + onRuntimeStarted: () => dispatch(setStartedState(true)), onTurboModeOn: () => dispatch(setTurboState(true)), onTurboModeOff: () => dispatch(setTurboState(false)), - onShowAlert: data => { - dispatch(showAlert(data)); + onShowExtensionAlert: data => { + dispatch(showExtensionAlert(data)); }, onMicListeningUpdate: listening => { dispatch(updateMicIndicator(listening)); diff --git a/src/lib/vm-manager-hoc.jsx b/src/lib/vm-manager-hoc.jsx index 3d7e9a2e9..926c25cd7 100644 --- a/src/lib/vm-manager-hoc.jsx +++ b/src/lib/vm-manager-hoc.jsx @@ -5,8 +5,8 @@ import {connect} from 'react-redux'; import VM from 'scratch-vm'; import AudioEngine from 'scratch-audio'; -import CloudProvider from '../lib/cloud-provider'; +import {setProjectUnchanged} from '../reducers/project-changed'; import { LoadingStates, getIsLoadingWithId, @@ -28,12 +28,15 @@ const vmManagerHOC = function (WrappedComponent) { ]); } componentDidMount () { - if (this.props.vm.initialized) return; - this.audioEngine = new AudioEngine(); - this.props.vm.attachAudioEngine(this.audioEngine); - this.props.vm.setCompatibilityMode(true); - this.props.vm.start(); - this.props.vm.initialized = true; + if (!this.props.vm.initialized) { + this.audioEngine = new AudioEngine(); + this.props.vm.attachAudioEngine(this.audioEngine); + this.props.vm.setCompatibilityMode(true); + this.props.vm.initialized = true; + } + if (!this.props.isPlayerOnly && !this.props.isStarted) { + this.props.vm.start(); + } } componentDidUpdate (prevProps) { // if project is in loading state, AND fonts are loaded, @@ -42,22 +45,28 @@ const vmManagerHOC = function (WrappedComponent) { (!prevProps.isLoadingWithId || !prevProps.fontsLoaded)) { this.loadProject(); } + // Start the VM if entering editor mode with an unstarted vm + if (!this.props.isPlayerOnly && !this.props.isStarted) { + this.props.vm.start(); + } } loadProject () { return this.props.vm.loadProject(this.props.projectData) .then(() => { this.props.onLoadedProject(this.props.loadingState, this.props.canSave); - // If the cloud host exists, open a cloud connection and - // set the vm's cloud provider. - if (this.props.cloudHost) { - // TODO check if we should actually - // connect to cloud data based on info from the loaded project and - // info about the user (e.g. scratcher status) - this.props.vm.setCloudProvider(new CloudProvider( - this.props.cloudHost, - this.props.vm, - this.props.username, - this.props.projectId)); + // Wrap in a setTimeout because skin loading in + // the renderer can be async. + setTimeout(() => this.props.onSetProjectUnchanged()); + + // If the vm is not running, call draw on the renderer manually + // This draws the state of the loaded project with no blocks running + // which closely matches the 2.0 behavior, except for monitors– + // 2.0 runs monitors and shows updates (e.g. timer monitor) + // before the VM starts running other hat blocks. + if (!this.props.isStarted) { + // Wrap in a setTimeout because skin loading in + // the renderer can be async. + setTimeout(() => this.props.vm.renderer.draw()); } }) .catch(e => { @@ -67,14 +76,13 @@ const vmManagerHOC = function (WrappedComponent) { render () { const { /* eslint-disable no-unused-vars */ - cloudHost, fontsLoaded, loadingState, + isStarted, onError: onErrorProp, onLoadedProject: onLoadedProjectProp, + onSetProjectUnchanged, projectData, - projectId, - username, /* eslint-enable no-unused-vars */ isLoadingWithId: isLoadingWithIdProp, vm, @@ -95,9 +103,11 @@ const vmManagerHOC = function (WrappedComponent) { cloudHost: PropTypes.string, fontsLoaded: PropTypes.bool, isLoadingWithId: PropTypes.bool, + isPlayerOnly: PropTypes.bool, loadingState: PropTypes.oneOf(LoadingStates), onError: PropTypes.func, onLoadedProject: PropTypes.func, + onSetProjectUnchanged: PropTypes.func, projectData: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), username: PropTypes.string, @@ -107,17 +117,21 @@ const vmManagerHOC = function (WrappedComponent) { const mapStateToProps = state => { const loadingState = state.scratchGui.projectState.loadingState; return { + fontsLoaded: state.scratchGui.fontsLoaded, isLoadingWithId: getIsLoadingWithId(loadingState), projectData: state.scratchGui.projectState.projectData, projectId: state.scratchGui.projectState.projectId, - loadingState: loadingState + loadingState: loadingState, + isPlayerOnly: state.scratchGui.mode.isPlayerOnly, + isStarted: state.scratchGui.vmStatus.started }; }; const mapDispatchToProps = dispatch => ({ onError: error => dispatch(projectError(error)), onLoadedProject: (loadingState, canSave) => - dispatch(onLoadedProject(loadingState, canSave)) + dispatch(onLoadedProject(loadingState, canSave, true)), + onSetProjectUnchanged: () => dispatch(setProjectUnchanged()) }); // Allow incoming props to override redux-provided props. Used to mock in tests. diff --git a/src/playground/index.jsx b/src/playground/index.jsx index ba0ada4da..dcf6fa181 100644 --- a/src/playground/index.jsx +++ b/src/playground/index.jsx @@ -1,6 +1,7 @@ // Polyfills import 'es6-object-assign/auto'; import 'core-js/fn/array/includes'; +import 'core-js/fn/promise/finally'; import 'intl'; // For Safari 9 import React from 'react'; diff --git a/src/playground/render-gui.jsx b/src/playground/render-gui.jsx index bc13e36f2..dc541e854 100644 --- a/src/playground/render-gui.jsx +++ b/src/playground/render-gui.jsx @@ -6,6 +6,23 @@ import AppStateHOC from '../lib/app-state-hoc.jsx'; import GUI from '../containers/gui.jsx'; import HashParserHOC from '../lib/hash-parser-hoc.jsx'; import TitledHOC from '../lib/titled-hoc.jsx'; +import log from '../lib/log.js'; + +const onClickLogo = () => { + window.location = 'https://scratch.mit.edu'; +}; + +const handleTelemetryModalCancel = () => { + log('User canceled telemetry modal'); +}; + +const handleTelemetryModalOptIn = () => { + log('User opted into telemetry'); +}; + +const handleTelemetryModalOptOut = () => { + log('User opted out of telemetry'); +}; /* * Render the GUI playground. This is a separate function because importing anything @@ -28,19 +45,42 @@ export default appTarget => { const backpackHostMatches = window.location.href.match(/[?&]backpack_host=([^&]*)&?/); const backpackHost = backpackHostMatches ? backpackHostMatches[1] : null; - const backpackOptions = { - visible: true, - host: backpackHost - }; + const scratchDesktopMatches = window.location.href.match(/[?&]isScratchDesktop=([^&]+)/); + let simulateScratchDesktop; + if (scratchDesktopMatches) { + try { + // parse 'true' into `true`, 'false' into `false`, etc. + simulateScratchDesktop = JSON.parse(scratchDesktopMatches[1]); + } catch { + // it's not JSON so just use the string + // note that a typo like "falsy" will be treated as true + simulateScratchDesktop = scratchDesktopMatches[1]; + } + } + if (process.env.NODE_ENV === 'production' && typeof window === 'object') { // Warn before navigating away window.onbeforeunload = () => true; } ReactDOM.render( - , + // important: this is checking whether `simulateScratchDesktop` is truthy, not just defined! + simulateScratchDesktop ? + : + , appTarget); }; diff --git a/src/reducers/alerts.js b/src/reducers/alerts.js index ee5ef0931..b5c82daf0 100644 --- a/src/reducers/alerts.js +++ b/src/reducers/alerts.js @@ -1,38 +1,103 @@ +import alertsData from '../lib/alerts/index.jsx'; +import {AlertTypes, AlertLevels} from '../lib/alerts/index.jsx'; import extensionData from '../lib/libraries/extensions/index.jsx'; -const CLOSE_ALERT = 'scratch-gui/alerts/CLOSE_ALERT'; const SHOW_ALERT = 'scratch-gui/alerts/SHOW_ALERT'; +const SHOW_EXTENSION_ALERT = 'scratch-gui/alerts/SHOW_EXTENSION_ALERT'; +const CLOSE_ALERT = 'scratch-gui/alerts/CLOSE_ALERT'; +const CLOSE_ALERTS_WITH_ID = 'scratch-gui/alerts/CLOSE_ALERTS_WITH_ID'; +/** + * Initial state of alerts reducer + * + * {bool} visible - whether the alerts are visible + * {array} alertsList - list of alerts, each with properties: + * * alertType (required): one of AlertTypes + * * closeButton (optional): bool indicating that we should show close button + * * content (optional): react element (a ) + * * extentionId (optional): id string that identifies the extension + * * iconURL (optional): string + * * level (required): string, one of AlertLevels + * * message (optional): string + * * showReconnect (optional): bool + */ const initialState = { visible: true, alertsList: [] }; +const filterPopupAlerts = alertsList => ( + alertsList.filter(curAlert => ( + curAlert.alertType === AlertTypes.STANDARD || + curAlert.alertType === AlertTypes.EXTENSION + )) +); + +const filterInlineAlerts = alertsList => ( + alertsList.filter(curAlert => ( + curAlert.alertType === AlertTypes.INLINE + )) +); + const reducer = function (state, action) { if (typeof state === 'undefined') state = initialState; switch (action.type) { - case SHOW_ALERT: { - const newList = state.alertsList.slice(); - const newAlert = {message: action.data.message}; - const extensionId = action.data.extensionId; - newAlert.showReconnect = false; - if (extensionId) { // if it's an extension - const extension = extensionData.find(ext => ext.extensionId === extensionId); - if (extension && extension.name) { - // TODO: is this the right place to assemble this message? - newAlert.extensionId = extensionId; - newAlert.message = `${newAlert.message} ${extension.name}.`; - newAlert.showReconnect = true; - } - if (extension && extension.smallPeripheralImage) { - newAlert.iconURL = extension.smallPeripheralImage; + case SHOW_ALERT: { // intended to show standard and inline alerts, but not extensions + const alertId = action.alertId; + if (alertId) { + const newAlert = { + alertId: alertId, + level: AlertLevels.WARN // default level + }; + const alertData = alertsData.find(thisAlertData => thisAlertData.alertId === alertId); + if (alertData) { + const newList = state.alertsList.filter(curAlert => ( + !alertData.clearList || alertData.clearList.indexOf(curAlert.alertId) === -1 + )); + if (action.data && action.data.message) { + newAlert.message = action.data.message; + } + + newAlert.alertType = alertData.alertType || AlertTypes.STANDARD; + newAlert.closeButton = alertData.closeButton; + newAlert.content = alertData.content; + newAlert.iconURL = alertData.iconURL; + newAlert.iconSpinner = alertData.iconSpinner; + newAlert.level = alertData.level; + newAlert.showDownload = alertData.showDownload; + newAlert.showSaveNow = alertData.showSaveNow; + + newList.push(newAlert); + return Object.assign({}, state, { + alertsList: newList + }); } } - // TODO: add cases for other kinds of alerts here? - newList.push(newAlert); - return Object.assign({}, state, { - alertsList: newList - }); + return state; // if alert not found, show nothing + } + case SHOW_EXTENSION_ALERT: { + const extensionId = action.data.extensionId; + if (extensionId) { + const extension = extensionData.find(ext => ext.extensionId === extensionId); + if (extension) { + const newList = state.alertsList.slice(); + const newAlert = { + alertType: AlertTypes.EXTENSION, + closeButton: true, + extensionId: extensionId, + extensionName: extension.name, + iconURL: extension.smallPeripheralImage, + level: AlertLevels.WARN, + showReconnect: true + }; + newList.push(newAlert); + + return Object.assign({}, state, { + alertsList: newList + }); + } + } + return state; // if alert not found, show nothing } case CLOSE_ALERT: { const newList = state.alertsList.slice(); @@ -41,13 +106,20 @@ const reducer = function (state, action) { alertsList: newList }); } + case CLOSE_ALERTS_WITH_ID: { + return Object.assign({}, state, { + alertsList: state.alertsList.filter(curAlert => ( + curAlert.alertId !== action.alertId + )) + }); + } default: return state; } }; /** - * Function to close an alert with the given index. + * Action creator to close an alert with the given index. * * @param {object} index - the index of the alert to close. * @return {object} - an object to be passed to the reducer. @@ -60,23 +132,72 @@ const closeAlert = function (index) { }; /** - * Function to show an alert with the given input data. + * Action creator to close all alerts with a given ID. + * + * @param {string} alertId - id string of the alert to close + * @return {object} - an object to be passed to the reducer. + */ +const closeAlertsWithId = function (alertId) { + return { + type: CLOSE_ALERTS_WITH_ID, + alertId + }; +}; + +/** + * Action creator to show an alert with the given alertId. + * + * @param {string} alertId - id string of the alert to show + * @return {object} - an object to be passed to the reducer. + */ +const showStandardAlert = function (alertId) { + return { + type: SHOW_ALERT, + alertId + }; +}; + +/** + * Action creator to show an alert with the given input data. * * @param {object} data - data for the alert * @param {string} data.message - message for the alert * @param {string} data.extensionId - extension ID for the alert * @return {object} - an object to be passed to the reducer. */ -const showAlert = function (data) { +const showExtensionAlert = function (data) { return { - type: SHOW_ALERT, + type: SHOW_EXTENSION_ALERT, data }; }; +/** + * Function to dispatch showing an alert, with optional + * timeout to make it close/go away. + * + * @param {object} dispatch - dispatch function + * @param {string} alertId - the ID of the alert + */ +const showAlertWithTimeout = function (dispatch, alertId) { + const alertData = alertsData.find(thisAlertData => thisAlertData.alertId === alertId); + if (alertData) { + dispatch(showStandardAlert(alertId)); + if (alertData.maxDisplaySecs) { + setTimeout(() => { + dispatch(closeAlertsWithId(alertId)); + }, alertData.maxDisplaySecs * 1000); + } + } +}; + export { reducer as default, initialState as alertsInitialState, closeAlert, - showAlert + filterInlineAlerts, + filterPopupAlerts, + showAlertWithTimeout, + showExtensionAlert, + showStandardAlert }; diff --git a/src/reducers/fonts-loaded.js b/src/reducers/fonts-loaded.js new file mode 100644 index 000000000..fcfdc58fa --- /dev/null +++ b/src/reducers/fonts-loaded.js @@ -0,0 +1,23 @@ +const SET_FONTS_LOADED = 'fontsLoaded/SET_FONTS_LOADED'; + +const initialState = false; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case SET_FONTS_LOADED: + return action.loaded; + default: + return state; + } +}; +const setFontsLoaded = () => ({ + type: SET_FONTS_LOADED, + loaded: true +}); + +export { + reducer as default, + initialState as fontsLoadedInitialState, + setFontsLoaded +}; diff --git a/src/reducers/gui.js b/src/reducers/gui.js index b41f72452..28b335b30 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -14,11 +14,14 @@ import modalReducer, {modalsInitialState} from './modals'; import modeReducer, {modeInitialState} from './mode'; import monitorReducer, {monitorsInitialState} from './monitors'; import monitorLayoutReducer, {monitorLayoutInitialState} from './monitor-layout'; +import projectChangedReducer, {projectChangedInitialState} from './project-changed'; import projectStateReducer, {projectStateInitialState} from './project-state'; import projectTitleReducer, {projectTitleInitialState} from './project-title'; +import fontsLoadedReducer, {fontsLoadedInitialState} from './fonts-loaded'; import restoreDeletionReducer, {restoreDeletionInitialState} from './restore-deletion'; import stageSizeReducer, {stageSizeInitialState} from './stage-size'; import targetReducer, {targetsInitialState} from './targets'; +import timeoutReducer, {timeoutInitialState} from './timeout'; import toolboxReducer, {toolboxInitialState} from './toolbox'; import vmReducer, {vmInitialState} from './vm'; import vmStatusReducer, {vmStatusInitialState} from './vm-status'; @@ -45,10 +48,13 @@ const guiInitialState = { modals: modalsInitialState, monitors: monitorsInitialState, monitorLayout: monitorLayoutInitialState, + projectChanged: projectChangedInitialState, projectState: projectStateInitialState, projectTitle: projectTitleInitialState, + fontsLoaded: fontsLoadedInitialState, restoreDeletion: restoreDeletionInitialState, targets: targetsInitialState, + timeout: timeoutInitialState, toolbox: toolboxInitialState, vm: vmInitialState, vmStatus: vmStatusInitialState @@ -60,7 +66,10 @@ const initPlayer = function (currentState) { currentState, {mode: { isFullScreen: currentState.mode.isFullScreen, - isPlayerOnly: true + isPlayerOnly: true, + // When initializing in player mode, make sure to reset + // hasEverEnteredEditorMode + hasEverEnteredEditor: false }} ); }; @@ -70,7 +79,21 @@ const initFullScreen = function (currentState) { currentState, {mode: { isFullScreen: true, - isPlayerOnly: currentState.mode.isPlayerOnly + isPlayerOnly: currentState.mode.isPlayerOnly, + hasEverEnteredEditor: currentState.mode.hasEverEnteredEditor + }} + ); +}; + +const initEmbedded = function (currentState) { + return Object.assign( + {}, + currentState, + {mode: { + showBranding: true, + isFullScreen: true, + isPlayerOnly: true, + hasEverEnteredEditor: false }} ); }; @@ -80,9 +103,6 @@ const initTutorialCard = function (currentState, deckId) { {}, currentState, { - modals: { - previewInfo: false - }, cards: { visible: true, content: decks, @@ -96,14 +116,25 @@ const initTutorialCard = function (currentState, deckId) { ); }; -const initTutorialLibrary = function (currentState) { +const initPreviewInfo = function (currentState) { return Object.assign( {}, currentState, { modals: { - previewInfo: false, - tipsLibrary: true + previewInfo: true // this key must match `MODAL_PREVIEW_INFO` in modals.js + } + } + ); +}; + +const initTelemetryModal = function (currentState) { + return Object.assign( + {}, + currentState, + { + modals: { + telemetryModal: true // this key must match `MODAL_TELEMETRY` in modals.js } } ); @@ -126,10 +157,13 @@ const guiReducer = combineReducers({ modals: modalReducer, monitors: monitorReducer, monitorLayout: monitorLayoutReducer, + projectChanged: projectChangedReducer, projectState: projectStateReducer, projectTitle: projectTitleReducer, + fontsLoaded: fontsLoadedReducer, restoreDeletion: restoreDeletionReducer, targets: targetReducer, + timeout: timeoutReducer, toolbox: toolboxReducer, vm: vmReducer, vmStatus: vmStatusReducer @@ -139,8 +173,10 @@ export { guiReducer as default, guiInitialState, guiMiddleware, + initEmbedded, initFullScreen, initPlayer, - initTutorialCard, - initTutorialLibrary + initPreviewInfo, + initTelemetryModal, + initTutorialCard }; diff --git a/src/reducers/modals.js b/src/reducers/modals.js index d385f2613..7b5cc2723 100644 --- a/src/reducers/modals.js +++ b/src/reducers/modals.js @@ -1,5 +1,3 @@ -import analytics from '../lib/analytics'; - const OPEN_MODAL = 'scratch-gui/modals/OPEN_MODAL'; const CLOSE_MODAL = 'scratch-gui/modals/CLOSE_MODAL'; @@ -10,6 +8,7 @@ const MODAL_EXTENSION_LIBRARY = 'extensionLibrary'; const MODAL_IMPORT_INFO = 'importInfo'; const MODAL_LOADING_PROJECT = 'loadingProject'; const MODAL_PREVIEW_INFO = 'previewInfo'; +const MODAL_TELEMETRY = 'telemetryModal'; const MODAL_SOUND_LIBRARY = 'soundLibrary'; const MODAL_SPRITE_LIBRARY = 'spriteLibrary'; const MODAL_SOUND_RECORDER = 'soundRecorder'; @@ -23,7 +22,8 @@ const initialState = { [MODAL_EXTENSION_LIBRARY]: false, [MODAL_IMPORT_INFO]: false, [MODAL_LOADING_PROJECT]: false, - [MODAL_PREVIEW_INFO]: true, + [MODAL_PREVIEW_INFO]: false, + [MODAL_TELEMETRY]: false, [MODAL_SOUND_LIBRARY]: false, [MODAL_SPRITE_LIBRARY]: false, [MODAL_SOUND_RECORDER]: false, @@ -59,51 +59,42 @@ const closeModal = function (modal) { }; }; const openBackdropLibrary = function () { - analytics.pageview('/libraries/backdrops'); return openModal(MODAL_BACKDROP_LIBRARY); }; const openCameraCapture = function () { - analytics.pageview('/modals/camera'); return openModal(MODAL_CAMERA_CAPTURE); }; const openCostumeLibrary = function () { - analytics.pageview('/libraries/costumes'); return openModal(MODAL_COSTUME_LIBRARY); }; const openExtensionLibrary = function () { - analytics.pageview('/libraries/extensions'); return openModal(MODAL_EXTENSION_LIBRARY); }; const openImportInfo = function () { - analytics.pageview('modals/import'); return openModal(MODAL_IMPORT_INFO); }; const openLoadingProject = function () { - analytics.pageview('modals/loading'); return openModal(MODAL_LOADING_PROJECT); }; const openPreviewInfo = function () { - analytics.pageview('/modals/preview'); return openModal(MODAL_PREVIEW_INFO); }; +const openTelemetryModal = function () { + return openModal(MODAL_TELEMETRY); +}; const openSoundLibrary = function () { - analytics.pageview('/libraries/sounds'); return openModal(MODAL_SOUND_LIBRARY); }; const openSpriteLibrary = function () { - analytics.pageview('/libraries/sprites'); return openModal(MODAL_SPRITE_LIBRARY); }; const openSoundRecorder = function () { - analytics.pageview('/modals/microphone'); return openModal(MODAL_SOUND_RECORDER); }; const openConnectionModal = function () { - analytics.pageview('/modals/connection'); return openModal(MODAL_CONNECTION); }; const openTipsLibrary = function () { - analytics.pageview('/modals/tips'); return openModal(MODAL_TIPS_LIBRARY); }; const closeBackdropLibrary = function () { @@ -127,6 +118,9 @@ const closeLoadingProject = function () { const closePreviewInfo = function () { return closeModal(MODAL_PREVIEW_INFO); }; +const closeTelemetryModal = function () { + return closeModal(MODAL_TELEMETRY); +}; const closeSpriteLibrary = function () { return closeModal(MODAL_SPRITE_LIBRARY); }; @@ -155,6 +149,7 @@ export { openSoundLibrary, openSpriteLibrary, openSoundRecorder, + openTelemetryModal, openTipsLibrary, openConnectionModal, closeBackdropLibrary, @@ -167,6 +162,7 @@ export { closeSpriteLibrary, closeSoundLibrary, closeSoundRecorder, + closeTelemetryModal, closeTipsLibrary, closeConnectionModal }; diff --git a/src/reducers/mode.js b/src/reducers/mode.js index d95cb1f99..e07b0cda0 100644 --- a/src/reducers/mode.js +++ b/src/reducers/mode.js @@ -2,8 +2,10 @@ const SET_FULL_SCREEN = 'scratch-gui/mode/SET_FULL_SCREEN'; const SET_PLAYER = 'scratch-gui/mode/SET_PLAYER'; const initialState = { + showBranding: false, isFullScreen: false, - isPlayerOnly: false + isPlayerOnly: false, + hasEverEnteredEditor: true }; const reducer = function (state, action) { @@ -17,7 +19,8 @@ const reducer = function (state, action) { case SET_PLAYER: return { isFullScreen: state.isFullScreen, - isPlayerOnly: action.isPlayerOnly + isPlayerOnly: action.isPlayerOnly, + hasEverEnteredEditor: state.hasEverEnteredEditor || !action.isPlayerOnly }; default: return state; diff --git a/src/reducers/project-changed.js b/src/reducers/project-changed.js new file mode 100644 index 000000000..c59f6a107 --- /dev/null +++ b/src/reducers/project-changed.js @@ -0,0 +1,28 @@ +const SET_PROJECT_CHANGED = 'scratch-gui/project-changed/SET_PROJECT_CHANGED'; + +const initialState = false; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case SET_PROJECT_CHANGED: + return action.changed; + default: + return state; + } +}; +const setProjectChanged = () => ({ + type: SET_PROJECT_CHANGED, + changed: true +}); +const setProjectUnchanged = () => ({ + type: SET_PROJECT_CHANGED, + changed: false +}); + +export { + reducer as default, + initialState as projectChangedInitialState, + setProjectChanged, + setProjectUnchanged +}; diff --git a/src/reducers/project-state.js b/src/reducers/project-state.js index e264f18a4..7ea21bd3a 100644 --- a/src/reducers/project-state.js +++ b/src/reducers/project-state.js @@ -9,34 +9,42 @@ const DONE_LOADING_VM_WITH_ID = 'scratch-gui/project-state/DONE_LOADING_VM_WITH_ const DONE_LOADING_VM_WITHOUT_ID = 'scratch-gui/project-state/DONE_LOADING_VM_WITHOUT_ID'; const DONE_REMIXING = 'scratch-gui/project-state/DONE_REMIXING'; const DONE_UPDATING = 'scratch-gui/project-state/DONE_UPDATING'; +const DONE_UPDATING_BEFORE_COPY = 'scratch-gui/project-state/DONE_UPDATING_BEFORE_COPY'; +const DONE_UPDATING_BEFORE_FILE_UPLOAD = 'scratch-gui/project-state/DONE_UPDATING_BEFORE_FILE_UPLOAD'; const DONE_UPDATING_BEFORE_NEW = 'scratch-gui/project-state/DONE_UPDATING_BEFORE_NEW'; +const RETURN_TO_SHOWING = 'scratch-gui/project-state/RETURN_TO_SHOWING'; const SET_PROJECT_ID = 'scratch-gui/project-state/SET_PROJECT_ID'; -const START_CREATING_COPY = 'scratch-gui/project-state/START_CREATING_COPY'; +const START_AUTO_UPDATING = 'scratch-gui/project-state/START_AUTO_UPDATING'; const START_CREATING_NEW = 'scratch-gui/project-state/START_CREATING_NEW'; const START_ERROR = 'scratch-gui/project-state/START_ERROR'; const START_FETCHING_NEW = 'scratch-gui/project-state/START_FETCHING_NEW'; const START_LOADING_VM_FILE_UPLOAD = 'scratch-gui/project-state/START_LOADING_FILE_UPLOAD'; +const START_MANUAL_UPDATING = 'scratch-gui/project-state/START_MANUAL_UPDATING'; const START_REMIXING = 'scratch-gui/project-state/START_REMIXING'; -const START_UPDATING = 'scratch-gui/project-state/START_UPDATING'; +const START_UPDATING_BEFORE_CREATING_COPY = 'scratch-gui/project-state/START_UPDATING_BEFORE_CREATING_COPY'; const START_UPDATING_BEFORE_CREATING_NEW = 'scratch-gui/project-state/START_UPDATING_BEFORE_CREATING_NEW'; +const START_UPDATING_BEFORE_FILE_UPLOAD = 'scratch-gui/project-state/START_UPDATING_BEFORE_FILE_UPLOAD'; const defaultProjectId = '0'; // hardcoded id of default project const LoadingState = keyMirror({ NOT_LOADED: null, ERROR: null, - FETCHING_WITH_ID: null, + AUTO_UPDATING: null, + CREATING_COPY: null, + CREATING_NEW: null, FETCHING_NEW_DEFAULT: null, - LOADING_VM_WITH_ID: null, + FETCHING_WITH_ID: null, LOADING_VM_FILE_UPLOAD: null, LOADING_VM_NEW_DEFAULT: null, + LOADING_VM_WITH_ID: null, + MANUAL_UPDATING: null, REMIXING: null, SHOWING_WITH_ID: null, SHOWING_WITHOUT_ID: null, - CREATING_COPY: null, - UPDATING: null, - UPDATING_BEFORE_NEW: null, - CREATING_NEW: null + UPDATING_BEFORE_COPY: null, + UPDATING_BEFORE_FILE_UPLOAD: null, + UPDATING_BEFORE_NEW: null }); const LoadingStates = Object.keys(LoadingState); @@ -54,17 +62,36 @@ const getIsLoadingWithId = loadingState => ( loadingState === LoadingState.LOADING_VM_WITH_ID || loadingState === LoadingState.LOADING_VM_NEW_DEFAULT ); +const getIsLoading = loadingState => ( + loadingState === LoadingState.LOADING_VM_FILE_UPLOAD || + loadingState === LoadingState.LOADING_VM_WITH_ID || + loadingState === LoadingState.LOADING_VM_NEW_DEFAULT +); +const getIsLoadingUpload = loadingState => ( + loadingState === LoadingState.LOADING_VM_FILE_UPLOAD +); const getIsCreatingNew = loadingState => ( loadingState === LoadingState.CREATING_NEW ); +const getIsAnyCreatingNewState = loadingState => ( + loadingState === LoadingState.FETCHING_NEW_DEFAULT || + loadingState === LoadingState.LOADING_VM_NEW_DEFAULT || + loadingState === LoadingState.CREATING_NEW +); const getIsCreatingCopy = loadingState => ( loadingState === LoadingState.CREATING_COPY ); +const getIsManualUpdating = loadingState => ( + loadingState === LoadingState.MANUAL_UPDATING +); const getIsRemixing = loadingState => ( loadingState === LoadingState.REMIXING ); const getIsUpdating = loadingState => ( - loadingState === LoadingState.UPDATING || + loadingState === LoadingState.AUTO_UPDATING || + loadingState === LoadingState.MANUAL_UPDATING || + loadingState === LoadingState.UPDATING_BEFORE_COPY || + loadingState === LoadingState.UPDATING_BEFORE_FILE_UPLOAD || loadingState === LoadingState.UPDATING_BEFORE_NEW ); const getIsShowingProject = loadingState => ( @@ -122,7 +149,8 @@ const reducer = function (state, action) { if (state.loadingState === LoadingState.LOADING_VM_FILE_UPLOAD || state.loadingState === LoadingState.LOADING_VM_NEW_DEFAULT) { return Object.assign({}, state, { - loadingState: LoadingState.SHOWING_WITHOUT_ID + loadingState: LoadingState.SHOWING_WITHOUT_ID, + projectId: defaultProjectId }); } return state; @@ -136,7 +164,7 @@ const reducer = function (state, action) { case DONE_LOADING_VM_TO_SAVE: if (state.loadingState === LoadingState.LOADING_VM_FILE_UPLOAD) { return Object.assign({}, state, { - loadingState: LoadingState.UPDATING + loadingState: LoadingState.AUTO_UPDATING }); } return state; @@ -161,12 +189,27 @@ const reducer = function (state, action) { } return state; case DONE_UPDATING: - if (state.loadingState === LoadingState.UPDATING) { + if (state.loadingState === LoadingState.AUTO_UPDATING || + state.loadingState === LoadingState.MANUAL_UPDATING) { return Object.assign({}, state, { loadingState: LoadingState.SHOWING_WITH_ID }); } return state; + case DONE_UPDATING_BEFORE_COPY: + if (state.loadingState === LoadingState.UPDATING_BEFORE_COPY) { + return Object.assign({}, state, { + loadingState: LoadingState.CREATING_COPY + }); + } + return state; + case DONE_UPDATING_BEFORE_FILE_UPLOAD: + if (state.loadingState === LoadingState.UPDATING_BEFORE_FILE_UPLOAD) { + return Object.assign({}, state, { + loadingState: LoadingState.LOADING_VM_FILE_UPLOAD + }); + } + return state; case DONE_UPDATING_BEFORE_NEW: if (state.loadingState === LoadingState.UPDATING_BEFORE_NEW) { return Object.assign({}, state, { @@ -175,34 +218,64 @@ const reducer = function (state, action) { }); } return state; + case RETURN_TO_SHOWING: + if (state.projectId === null || state.projectId === defaultProjectId) { + return Object.assign({}, state, { + loadingState: LoadingState.SHOWING_WITHOUT_ID, + projectId: defaultProjectId + }); + } + return Object.assign({}, state, { + loadingState: LoadingState.SHOWING_WITH_ID + }); case SET_PROJECT_ID: // if the projectId hasn't actually changed do nothing if (state.projectId === action.projectId) { return state; } - // if setting the default project id, specifically fetch that project - if (action.projectId === defaultProjectId) { - return Object.assign({}, state, { - loadingState: LoadingState.FETCHING_NEW_DEFAULT, - projectId: defaultProjectId - }); - } // if we were already showing a project, and a different projectId is set, only fetch that project if // projectId has changed. This prevents re-fetching projects unnecessarily. if (state.loadingState === LoadingState.SHOWING_WITH_ID) { - if (state.projectId !== action.projectId) { + // if setting the default project id, specifically fetch that project + if (action.projectId === defaultProjectId || action.projectId === null) { + return Object.assign({}, state, { + loadingState: LoadingState.FETCHING_NEW_DEFAULT, + projectId: defaultProjectId + }); + } + return Object.assign({}, state, { + loadingState: LoadingState.FETCHING_WITH_ID, + projectId: action.projectId + }); + } else if (state.loadingState === LoadingState.SHOWING_WITHOUT_ID) { + // if we were showing a project already, don't transition to default project. + if (action.projectId !== defaultProjectId && action.projectId !== null) { return Object.assign({}, state, { loadingState: LoadingState.FETCHING_WITH_ID, projectId: action.projectId }); } } else { // allow any other states to transition to fetching project + // if setting the default project id, specifically fetch that project + if (action.projectId === defaultProjectId || action.projectId === null) { + return Object.assign({}, state, { + loadingState: LoadingState.FETCHING_NEW_DEFAULT, + projectId: defaultProjectId + }); + } return Object.assign({}, state, { loadingState: LoadingState.FETCHING_WITH_ID, projectId: action.projectId }); } return state; + case START_AUTO_UPDATING: + if (state.loadingState === LoadingState.SHOWING_WITH_ID) { + return Object.assign({}, state, { + loadingState: LoadingState.AUTO_UPDATING + }); + } + return state; case START_CREATING_NEW: if (state.loadingState === LoadingState.SHOWING_WITHOUT_ID) { return Object.assign({}, state, { @@ -228,31 +301,28 @@ const reducer = function (state, action) { LoadingState.SHOWING_WITHOUT_ID ].includes(state.loadingState)) { return Object.assign({}, state, { - loadingState: LoadingState.LOADING_VM_FILE_UPLOAD, - projectId: null // clear any current projectId + loadingState: LoadingState.LOADING_VM_FILE_UPLOAD + }); + } + return state; + case START_MANUAL_UPDATING: + if (state.loadingState === LoadingState.SHOWING_WITH_ID) { + return Object.assign({}, state, { + loadingState: LoadingState.MANUAL_UPDATING }); } return state; case START_REMIXING: - // do not set projectId to null, because we nay reference it in creating project if (state.loadingState === LoadingState.SHOWING_WITH_ID) { return Object.assign({}, state, { loadingState: LoadingState.REMIXING }); } return state; - case START_UPDATING: + case START_UPDATING_BEFORE_CREATING_COPY: if (state.loadingState === LoadingState.SHOWING_WITH_ID) { return Object.assign({}, state, { - loadingState: LoadingState.UPDATING - }); - } - return state; - case START_CREATING_COPY: - // do not set projectId to null, because we nay reference it in creating project - if (state.loadingState === LoadingState.SHOWING_WITH_ID) { - return Object.assign({}, state, { - loadingState: LoadingState.CREATING_COPY + loadingState: LoadingState.UPDATING_BEFORE_COPY }); } return state; @@ -263,24 +333,55 @@ const reducer = function (state, action) { }); } return state; + case START_UPDATING_BEFORE_FILE_UPLOAD: + if (state.loadingState === LoadingState.SHOWING_WITH_ID) { + return Object.assign({}, state, { + loadingState: LoadingState.UPDATING_BEFORE_FILE_UPLOAD + }); + } + return state; case START_ERROR: - // NOTE: we should introduce handling in components for showing ERROR state + // fatal errors: there's no correct editor state for us to show if ([ - LoadingState.CREATING_NEW, LoadingState.FETCHING_NEW_DEFAULT, LoadingState.FETCHING_WITH_ID, LoadingState.LOADING_VM_NEW_DEFAULT, - LoadingState.LOADING_VM_WITH_ID, - LoadingState.REMIXING, - LoadingState.CREATING_COPY, - LoadingState.UPDATING_BEFORE_NEW, - LoadingState.UPDATING + LoadingState.LOADING_VM_WITH_ID ].includes(state.loadingState)) { return Object.assign({}, state, { loadingState: LoadingState.ERROR, error: action.error }); } + // non-fatal errors: can keep showing editor state fine + if ([ + LoadingState.AUTO_UPDATING, + LoadingState.CREATING_COPY, + LoadingState.MANUAL_UPDATING, + LoadingState.REMIXING, + LoadingState.UPDATING_BEFORE_COPY, + LoadingState.UPDATING_BEFORE_FILE_UPLOAD, + LoadingState.UPDATING_BEFORE_NEW + ].includes(state.loadingState)) { + return Object.assign({}, state, { + loadingState: LoadingState.SHOWING_WITH_ID, + error: action.error + }); + } + // non-fatal error; state to show depends on whether project we're showing + // has an id or not + if (state.loadingState === LoadingState.CREATING_NEW) { + if (state.projectId === defaultProjectId || state.projectId === null) { + return Object.assign({}, state, { + loadingState: LoadingState.SHOWING_WITHOUT_ID, + error: action.error + }); + } + return Object.assign({}, state, { + loadingState: LoadingState.SHOWING_WITH_ID, + error: action.error + }); + } return state; default: return state; @@ -330,36 +431,50 @@ const onFetchedProjectData = (projectData, loadingState) => { } }; -const onLoadedProject = (loadingState, canSave) => { - switch (loadingState) { - case LoadingState.LOADING_VM_WITH_ID: - return { - type: DONE_LOADING_VM_WITH_ID - }; - case LoadingState.LOADING_VM_FILE_UPLOAD: - if (canSave) { +const onLoadedProject = (loadingState, canSave, success) => { + if (success) { + switch (loadingState) { + case LoadingState.LOADING_VM_WITH_ID: return { - type: DONE_LOADING_VM_TO_SAVE + type: DONE_LOADING_VM_WITH_ID }; + case LoadingState.LOADING_VM_FILE_UPLOAD: + if (canSave) { + return { + type: DONE_LOADING_VM_TO_SAVE + }; + } + return { + type: DONE_LOADING_VM_WITHOUT_ID + }; + case LoadingState.LOADING_VM_NEW_DEFAULT: + return { + type: DONE_LOADING_VM_WITHOUT_ID + }; + default: + return; } - return { - type: DONE_LOADING_VM_WITHOUT_ID - }; - case LoadingState.LOADING_VM_NEW_DEFAULT: - return { - type: DONE_LOADING_VM_WITHOUT_ID - }; - default: - break; } + return { + type: RETURN_TO_SHOWING + }; }; const doneUpdatingProject = loadingState => { switch (loadingState) { - case LoadingState.UPDATING: + case LoadingState.AUTO_UPDATING: + case LoadingState.MANUAL_UPDATING: return { type: DONE_UPDATING }; + case LoadingState.UPDATING_BEFORE_COPY: + return { + type: DONE_UPDATING_BEFORE_COPY + }; + case LoadingState.UPDATING_BEFORE_FILE_UPLOAD: + return { + type: DONE_UPDATING_BEFORE_FILE_UPLOAD + }; case LoadingState.UPDATING_BEFORE_NEW: return { type: DONE_UPDATING_BEFORE_NEW @@ -384,16 +499,32 @@ const requestNewProject = needSave => { return {type: START_FETCHING_NEW}; }; -const onProjectUploadStarted = () => ({ - type: START_LOADING_VM_FILE_UPLOAD +const requestProjectUpload = loadingState => { + switch (loadingState) { + case LoadingState.SHOWING_WITH_ID: + return { + type: START_UPDATING_BEFORE_FILE_UPLOAD + }; + case LoadingState.NOT_LOADED: + case LoadingState.SHOWING_WITHOUT_ID: + return { + type: START_LOADING_VM_FILE_UPLOAD + }; + default: + break; + } +}; + +const autoUpdateProject = () => ({ + type: START_AUTO_UPDATING }); -const updateProject = () => ({ - type: START_UPDATING +const manualUpdateProject = () => ({ + type: START_MANUAL_UPDATING }); const saveProjectAsCopy = () => ({ - type: START_CREATING_COPY + type: START_UPDATING_BEFORE_CREATING_COPY }); const remixProject = () => ({ @@ -405,28 +536,33 @@ export { initialState as projectStateInitialState, LoadingState, LoadingStates, + autoUpdateProject, createProject, defaultProjectId, doneCreatingProject, doneUpdatingProject, + getIsAnyCreatingNewState, getIsCreatingCopy, getIsCreatingNew, getIsError, getIsFetchingWithId, getIsFetchingWithoutId, + getIsLoading, getIsLoadingWithId, + getIsLoadingUpload, + getIsManualUpdating, getIsRemixing, getIsShowingProject, getIsShowingWithId, getIsShowingWithoutId, getIsUpdating, + manualUpdateProject, onFetchedProjectData, onLoadedProject, - onProjectUploadStarted, projectError, remixProject, requestNewProject, + requestProjectUpload, saveProjectAsCopy, - setProjectId, - updateProject + setProjectId }; diff --git a/src/reducers/targets.js b/src/reducers/targets.js index 17a73f7d8..7da81982a 100644 --- a/src/reducers/targets.js +++ b/src/reducers/targets.js @@ -39,10 +39,7 @@ const updateTargets = function (targetList, editingTarget) { return { type: UPDATE_TARGET_LIST, targets: targetList, - editingTarget: editingTarget, - meta: { - throttle: 30 - } + editingTarget: editingTarget }; }; const highlightTarget = function (targetId) { diff --git a/src/reducers/timeout.js b/src/reducers/timeout.js new file mode 100644 index 000000000..5ed5b7be3 --- /dev/null +++ b/src/reducers/timeout.js @@ -0,0 +1,27 @@ +const SET_AUTOSAVE_TIMEOUT_ID = 'timeout/SET_AUTOSAVE_TIMEOUT_ID'; + +const initialState = { + autoSaveTimeoutId: null +}; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case SET_AUTOSAVE_TIMEOUT_ID: + return Object.assign({}, state, { + autoSaveTimeoutId: action.id + }); + default: + return state; + } +}; +const setAutoSaveTimeoutId = id => ({ + type: SET_AUTOSAVE_TIMEOUT_ID, + id +}); + +export { + reducer as default, + initialState as timeoutInitialState, + setAutoSaveTimeoutId +}; diff --git a/src/reducers/vm-status.js b/src/reducers/vm-status.js index 8e9651992..e99b80368 100644 --- a/src/reducers/vm-status.js +++ b/src/reducers/vm-status.js @@ -1,14 +1,20 @@ const SET_RUNNING_STATE = 'scratch-gui/vm-status/SET_RUNNING_STATE'; const SET_TURBO_STATE = 'scratch-gui/vm-status/SET_TURBO_STATE'; +const SET_STARTED_STATE = 'scratch-gui/vm-status/SET_STARTED_STATE'; const initialState = { running: false, + started: false, turbo: false }; const reducer = function (state, action) { if (typeof state === 'undefined') state = initialState; switch (action.type) { + case SET_STARTED_STATE: + return Object.assign({}, state, { + started: action.started + }); case SET_RUNNING_STATE: return Object.assign({}, state, { running: action.running @@ -22,6 +28,14 @@ const reducer = function (state, action) { } }; +const setStartedState = function (started) { + return { + type: SET_STARTED_STATE, + started: started + }; +}; + + const setRunningState = function (running) { return { type: SET_RUNNING_STATE, @@ -40,5 +54,6 @@ export { reducer as default, initialState as vmStatusInitialState, setRunningState, + setStartedState, setTurboState }; diff --git a/test/__mocks__/audio-buffer-player.js b/test/__mocks__/audio-buffer-player.js index 476b2b649..560bfb2d2 100644 --- a/test/__mocks__/audio-buffer-player.js +++ b/test/__mocks__/audio-buffer-player.js @@ -3,7 +3,8 @@ export default class MockAudioBufferPlayer { this.samples = samples; this.sampleRate = sampleRate; this.buffer = { - getChannelData: jest.fn(() => samples) + getChannelData: jest.fn(() => samples), + sampleRate: sampleRate }; this.play = jest.fn((trimStart, trimEnd, onUpdate) => { this.onUpdate = onUpdate; diff --git a/test/helpers/selenium-helper.js b/test/helpers/selenium-helper.js index 2d979e448..12549e933 100644 --- a/test/helpers/selenium-helper.js +++ b/test/helpers/selenium-helper.js @@ -14,6 +14,7 @@ class SeleniumHelper { 'clickText', 'clickButton', 'clickXpath', + 'elementIsVisible', 'findByText', 'findByXpath', 'getDriver', @@ -24,6 +25,10 @@ class SeleniumHelper { ]); } + elementIsVisible (element) { + return this.driver.wait(until.elementIsVisible(element)); + } + get scope () { // List of useful xpath scopes for finding elements return { @@ -43,6 +48,10 @@ class SeleniumHelper { if (USE_HEADLESS) { args.push('--headless'); } + + // Stub getUserMedia to always not allow access + args.push('--use-fake-ui-for-media-stream=deny'); + chromeCapabilities.set('chromeOptions', {args}); chromeCapabilities.setLoggingPrefs({ performance: 'ALL' diff --git a/test/integration/backdrops.test.js b/test/integration/backdrops.test.js new file mode 100644 index 000000000..9a527b2c9 --- /dev/null +++ b/test/integration/backdrops.test.js @@ -0,0 +1,52 @@ +import path from 'path'; +import SeleniumHelper from '../helpers/selenium-helper'; + +const { + clickText, + clickXpath, + findByXpath, + getDriver, + getLogs, + loadUri, + scope +} = new SeleniumHelper(); + +const uri = path.resolve(__dirname, '../../build/index.html'); + +let driver; + +describe('Working with backdrops', () => { + beforeAll(() => { + driver = getDriver(); + }); + + afterAll(async () => { + await driver.quit(); + }); + + test('Adding a backdrop from the library', async () => { + await loadUri(uri); + await clickXpath('//button[@title="Try It"]'); + + // Start on the sounds tab of sprite1 to test switching behavior + await clickText('Sounds'); + + // Add a backdrop without selecting the stage first to test switching + await clickXpath('//button[@aria-label="Choose a Backdrop"]'); + const el = await findByXpath("//input[@placeholder='Search']"); + await el.sendKeys('blue'); + await clickText('Blue Sky'); // Adds the backdrop + + // Make sure the stage is selected and the sound tab remains selected. + // This is different from Scratch2 which selected backdrop tab automatically + // See issue #3500 + await clickText('pop', scope.soundsTab); + + // Make sure the backdrop was actually added by going to the backdrops tab + await clickText('Backdrops'); + await clickText('Blue Sky', scope.costumesTab); + + const logs = await getLogs(); + await expect(logs).toEqual([]); + }); +}); diff --git a/test/integration/blocks.test.js b/test/integration/blocks.test.js index 6ca2d279c..d70396032 100644 --- a/test/integration/blocks.test.js +++ b/test/integration/blocks.test.js @@ -166,8 +166,66 @@ describe('Working with the blocks', () => { await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation await clickText('Meow', scope.blocksTab); // Click "play sound until done" block await clickText('record'); // Click "record..." option in the block's sound menu + // Access has been force denied, so close the alert that comes up + await driver.sleep(1000); // getUserMedia requests are very slow to fail for some reason + await driver.switchTo().alert() + .accept(); await findByText('Record Sound'); // Sound recorder is open const logs = await getLogs(); await expect(logs).toEqual([]); }); + + test('Renaming costume changes the default costume name in the toolbox', async () => { + await loadUri(uri); + await clickXpath('//button[@title="Try It"]'); + + // Rename the costume + await clickText('Costumes'); + const el = await findByXpath("//input[@value='costume1']"); + await el.sendKeys('newname'); + + // Make sure it is updated in the block menu + await clickText('Code'); + await clickText('Looks', scope.blocksTab); + await driver.sleep(500); // Wait for scroll to finish + await clickText('newname', scope.blocksTab); + }); + + // NOTE: This test describes the current behavior so that changes are not + // introduced inadvertly, but I know this is not the desired behavior + test('Adding costumes DOES NOT update the default costume name in the toolbox', async () => { + await loadUri(uri); + await clickXpath('//button[@title="Try It"]'); + + // By default, costume1 is in the costume tab + await clickText('Looks', scope.blocksTab); + await driver.sleep(500); // Wait for scroll to finish + await clickText('costume1', scope.blocksTab); + + // Also check that adding a new costume does not update the list + await clickText('Costumes'); + const el = await findByXpath('//button[@aria-label="Choose a Costume"]'); + await driver.actions().mouseMove(el) + .perform(); + await driver.sleep(500); // Wait for thermometer menu to come up + await clickXpath('//button[@aria-label="Paint"]'); + await clickText('costume3', scope.costumesTab); + // Check that the menu has not been updated + await clickText('Code'); + await clickText('costume1', scope.blocksTab); + }); + + // NOTE: This test describes the current behavior so that changes are not + // introduced inadvertly, but I know this is not the desired behavior + test('Adding a sound DOES NOT update the default sound name in the toolbox', async () => { + await loadUri(uri); + await clickXpath('//button[@title="Try It"]'); + await clickText('Sounds'); + await clickXpath('//button[@aria-label="Choose a Sound"]'); + await clickText('A Bass', scope.modal); // Should close the modal + await clickText('Code'); + await clickText('Sound', scope.blocksTab); + await driver.sleep(500); // Wait for scroll to finish + await clickText('Meow', scope.blocksTab); // Meow, not A Bass + }); }); diff --git a/test/integration/costumes.test.js b/test/integration/costumes.test.js index 78c7b911e..5181c429a 100644 --- a/test/integration/costumes.test.js +++ b/test/integration/costumes.test.js @@ -80,17 +80,6 @@ describe('Working with costumes', () => { await expect(logs).toEqual([]); }); - test('Adding a backdrop', async () => { - await loadUri(uri); - await clickXpath('//button[@title="Try It"]'); - await clickXpath('//button[@aria-label="Choose a Backdrop"]'); - const el = await findByXpath("//input[@placeholder='Search']"); - await el.sendKeys('blue'); - await clickText('Blue Sky'); // Should close the modal - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - test('Converting bitmap/vector in paint editor', async () => { await loadUri(uri); await clickXpath('//button[@title="Try It"]'); @@ -160,4 +149,36 @@ describe('Working with costumes', () => { const logs = await getLogs(); await expect(logs).toEqual([]); }); + + test('Adding a letter costume through the Letters filter in the library', async () => { + await loadUri(uri); + await driver.manage() + .window() + .setSize(1244, 768); // Letters filter not visible at 1024 width + await clickXpath('//button[@title="Try It"]'); + await clickText('Costumes'); + await clickXpath('//button[@aria-label="Choose a Costume"]'); + await clickText('Letters'); + await clickText('Block-a', scope.modal); // Closes modal + await rightClickText('Block-a', scope.costumesTab); // Make sure it is there + const logs = await getLogs(); + await expect(logs).toEqual([]); + }); + + test('Costumes animate on mouseover', async () => { + await loadUri(uri); + await clickXpath('//button[@title="Try It"]'); + await clickXpath('//button[@aria-label="Choose a Sprite"]'); + const searchElement = await findByXpath("//input[@placeholder='Search']"); + await searchElement.sendKeys('abb'); + const abbyElement = await findByXpath('//*[span[text()="Abby"]]'); + driver.actions() + .mouseMove(abbyElement) + .perform(); + // wait for one of Abby's alternate costumes to appear + await findByXpath('//img[@src="https://cdn.assets.scratch.mit.edu/internalapi/asset/b6e23922f23b49ddc6f62f675e77417c.svg/get/"]'); + const logs = await getLogs(); + await expect(logs).toEqual([]); + }); + }); diff --git a/test/integration/examples.test.js b/test/integration/examples.test.js index 8408c88f3..7e8ff7a10 100644 --- a/test/integration/examples.test.js +++ b/test/integration/examples.test.js @@ -40,9 +40,9 @@ describe('player example', () => { .then(pLogs => pLogs.map(log => JSON.parse(log.message).message) .filter(m => m.method === 'Network.requestWillBeSent') .map(m => m.params.request.url) - .filter(url => url === 'https://projects.scratch.mit.edu/internalapi/project/96708228/get/') + .filter(url => url === 'https://projects.scratch.mit.edu/96708228') ); - await expect(projectRequests).toEqual(['https://projects.scratch.mit.edu/internalapi/project/96708228/get/']); + await expect(projectRequests).toEqual(['https://projects.scratch.mit.edu/96708228']); }); }); @@ -71,9 +71,9 @@ describe('blocks example', () => { .then(pLogs => pLogs.map(log => JSON.parse(log.message).message) .filter(m => m.method === 'Network.requestWillBeSent') .map(m => m.params.request.url) - .filter(url => url === 'https://projects.scratch.mit.edu/internalapi/project/96708228/get/') + .filter(url => url === 'https://projects.scratch.mit.edu/96708228') ); - await expect(projectRequests).toEqual(['https://projects.scratch.mit.edu/internalapi/project/96708228/get/']); + await expect(projectRequests).toEqual(['https://projects.scratch.mit.edu/96708228']); }); test('Change categories', async () => { diff --git a/test/integration/menu-bar.test.js b/test/integration/menu-bar.test.js index ca1a4308b..6529b7bc4 100644 --- a/test/integration/menu-bar.test.js +++ b/test/integration/menu-bar.test.js @@ -2,6 +2,7 @@ import path from 'path'; import SeleniumHelper from '../helpers/selenium-helper'; const { + clickText, clickXpath, findByXpath, getDriver, @@ -31,24 +32,24 @@ describe('Menu bar settings', () => { await findByXpath('//*[li[span[text()="New"]] and not(@data-tip="tooltip")]'); }); - test('File->Upload should be enabled', async () => { + test('File->Load should be enabled', async () => { await loadUri(uri); await clickXpath('//button[@title="Try It"]'); await clickXpath( '//div[contains(@class, "menu-bar_menu-bar-item") and ' + 'contains(@class, "menu-bar_hoverable")][span[text()="File"]]' ); - await findByXpath('//*[li[span[text()="Upload from your computer"]] and not(@data-tip="tooltip")]'); + await findByXpath('//*[li[span[text()="Load from your computer"]] and not(@data-tip="tooltip")]'); }); - test('File->Download should be enabled', async () => { + test('File->Save should be enabled', async () => { await loadUri(uri); await clickXpath('//button[@title="Try It"]'); await clickXpath( '//div[contains(@class, "menu-bar_menu-bar-item") and ' + 'contains(@class, "menu-bar_hoverable")][span[text()="File"]]' ); - await findByXpath('//*[li[span[text()="Download to your computer"]] and not(@data-tip="tooltip")]'); + await findByXpath('//*[li[span[text()="Save to your computer"]] and not(@data-tip="tooltip")]'); }); test('Share button should NOT be enabled', async () => { @@ -56,4 +57,21 @@ describe('Menu bar settings', () => { await clickXpath('//button[@title="Try It"]'); await findByXpath('//div[span[div[span[text()="Share"]]] and @data-tip="tooltip"]'); }); + + test('Logo should be clickable', async () => { + await loadUri(uri); + await clickXpath('//button[@title="Try It"]'); + await clickXpath('//img[@alt="Scratch"]'); + const currentUrl = await driver.getCurrentUrl(); + await expect(currentUrl).toEqual('https://scratch.mit.edu/'); + }); + + test('(GH#4064) Project name should be editable', async () => { + await loadUri(uri); + await clickXpath('//button[@title="Try It"]'); + const el = await findByXpath('//input[@value="Scratch Project"]'); + await el.sendKeys(' - Personalized'); + await clickText('Costumes'); // just to blur the input + await clickXpath('//input[@value="Scratch Project - Personalized"]'); + }); }); diff --git a/test/integration/project-loading.test.js b/test/integration/project-loading.test.js index 09023632e..384463062 100644 --- a/test/integration/project-loading.test.js +++ b/test/integration/project-loading.test.js @@ -7,7 +7,8 @@ const { findByXpath, getDriver, getLogs, - loadUri + loadUri, + scope } = new SeleniumHelper(); const uri = path.resolve(__dirname, '../../build/index.html'); @@ -47,6 +48,11 @@ describe('Loading scratch gui', () => { await expect(logs).toEqual([]); }); + test('Nonexistent projects show error screen', async () => { + await loadUri(`${uri}#999999999999999999999`); + await clickText('Oops! Something went wrong.'); + }); + test('Invalid url when loading project through modal lets you try again', async () => { await loadUri(uri); await clickText('View 2.0 Project'); @@ -105,5 +111,64 @@ describe('Loading scratch gui', () => { const logs = await getLogs(); await expect(logs).toEqual([]); }); + + test('Creating new project resets active tab to Code tab', async () => { + await loadUri(uri); + await clickText('View 2.0 Project'); + const inputElement = await findByXpath("//input[@placeholder='scratch.mit.edu/projects/123456789']"); + const projectId = '96708228'; + await inputElement.sendKeys(`scratch.mit.edu/projects/${projectId}`); + await clickXpath('//button[@title="View Project"]'); + await new Promise(resolve => setTimeout(resolve, 2000)); + await findByXpath('//*[span[text()="Costumes"]]'); + await clickText('Costumes'); + await clickXpath( + '//div[contains(@class, "menu-bar_menu-bar-item") and ' + + 'contains(@class, "menu-bar_hoverable")][span[text()="File"]]' + ); + await clickXpath('//li[span[text()="New"]]'); + await findByXpath('//*[div[@class="scratchCategoryMenu"]]'); + await clickText('Operators', scope.blocksTab); + }); + + test('Not logged in->made no changes to project->create new project should not show alert', async () => { + await loadUri(uri); + await clickText('View 2.0 Project'); + const inputElement = await findByXpath("//input[@placeholder='scratch.mit.edu/projects/123456789']"); + const projectId = '96708228'; + await inputElement.sendKeys(`scratch.mit.edu/projects/${projectId}`); + await clickXpath('//button[@title="View Project"]'); + await new Promise(resolve => setTimeout(resolve, 2000)); + await clickXpath( + '//div[contains(@class, "menu-bar_menu-bar-item") and ' + + 'contains(@class, "menu-bar_hoverable")][span[text()="File"]]' + ); + await clickXpath('//li[span[text()="New"]]'); + await findByXpath('//*[div[@class="scratchCategoryMenu"]]'); + await clickText('Operators', scope.blocksTab); + }); + + test('Not logged in->made a change to project->create new project should show alert', async () => { + await loadUri(uri); + await clickText('View 2.0 Project'); + const inputElement = await findByXpath("//input[@placeholder='scratch.mit.edu/projects/123456789']"); + const projectId = '96708228'; + await inputElement.sendKeys(`scratch.mit.edu/projects/${projectId}`); + await clickXpath('//button[@title="View Project"]'); + await new Promise(resolve => setTimeout(resolve, 2000)); + await clickText('Sounds'); + await clickXpath('//button[@aria-label="Choose a Sound"]'); + await clickText('A Bass', scope.modal); // Should close the modal + await clickXpath( + '//div[contains(@class, "menu-bar_menu-bar-item") and ' + + 'contains(@class, "menu-bar_hoverable")][span[text()="File"]]' + ); + await clickXpath('//li[span[text()="New"]]'); + driver.switchTo() + .alert() + .accept(); + await findByXpath('//*[div[@class="scratchCategoryMenu"]]'); + await clickText('Operators', scope.blocksTab); + }); }); }); diff --git a/test/integration/sounds.test.js b/test/integration/sounds.test.js index ca1a3d275..3877ada3f 100644 --- a/test/integration/sounds.test.js +++ b/test/integration/sounds.test.js @@ -32,6 +32,7 @@ describe('Working with sounds', () => { // Delete the sound await rightClickText('Meow', scope.soundsTab); + await driver.sleep(500); // Wait a moment for context menu; only needed for local testing await clickText('delete', scope.soundsTab); // Add it back diff --git a/test/integration/sprites.test.js b/test/integration/sprites.test.js index ca6a84eba..a3dd48cf4 100644 --- a/test/integration/sprites.test.js +++ b/test/integration/sprites.test.js @@ -4,6 +4,7 @@ import SeleniumHelper from '../helpers/selenium-helper'; const { clickText, clickXpath, + elementIsVisible, findByText, findByXpath, getDriver, @@ -75,6 +76,17 @@ describe('Working with sprites', () => { await expect(logs).toEqual([]); }); + test('Deleting by x button on sprite tile', async () => { + await loadUri(uri); + await clickXpath('//button[@title="Try It"]'); + await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation + await clickXpath('//*[@aria-label="Close"]'); // Only visible close button is on the sprite + // Confirm that the stage has been switched to + await findByText('Stage selected: no motion blocks'); + const logs = await getLogs(); + await expect(logs).toEqual([]); + }); + test('Adding a sprite by uploading a png', async () => { await loadUri(uri); await clickXpath('//button[@title="Try It"]'); @@ -91,7 +103,7 @@ describe('Working with sprites', () => { // This test fails because uploading an SVG as a sprite changes the scaling // Enable when this is fixed issues/3608 - test.skip('Adding a sprite by uploading an svg (gh-3608)', async () => { + test('Adding a sprite by uploading an svg (gh-3608)', async () => { await loadUri(uri); await clickXpath('//button[@title="Try It"]'); const el = await findByXpath('//button[@aria-label="Choose a Sprite"]'); @@ -109,4 +121,42 @@ describe('Working with sprites', () => { const logs = await getLogs(); await expect(logs).toEqual([]); }); + + test('Adding a letter sprite through the Letters filter in the library', async () => { + await loadUri(uri); + await driver.manage() + .window() + .setSize(1244, 768); // Letters filter not visible at 1024 width + await clickXpath('//button[@title="Try It"]'); + await clickText('Costumes'); + await clickXpath('//button[@aria-label="Choose a Sprite"]'); + await clickText('Letters'); + await clickText('Block-B', scope.modal); // Closes modal + await rightClickText('Block-B', scope.spriteTile); // Make sure it is there + const logs = await getLogs(); + await expect(logs).toEqual([]); + }); + + test('Use browser back button to close library', async () => { + await driver.get('https://www.google.com'); + await loadUri(uri); + await clickXpath('//button[@title="Try It"]'); + await clickText('Costumes'); + await clickXpath('//button[@aria-label="Choose a Sprite"]'); + const abbyElement = await findByText('Abby'); // Should show editor for new costume + await elementIsVisible(abbyElement); + await driver.navigate().back(); + try { + // should throw error because library is no longer visible + await elementIsVisible(abbyElement); + throw 'ShouldNotGetHere'; // eslint-disable-line no-throw-literal + } catch (e) { + expect(e.constructor.name).toEqual('StaleElementReferenceError'); + } + const costumesElement = await findByText('Costumes'); // Should show editor for new costume + await elementIsVisible(costumesElement); + const logs = await getLogs(); + await expect(logs).toEqual([]); + }); + }); diff --git a/test/integration/tutorials-shortcut.test.js b/test/integration/tutorials-shortcut.test.js index 803e1e5ae..187e72525 100644 --- a/test/integration/tutorials-shortcut.test.js +++ b/test/integration/tutorials-shortcut.test.js @@ -9,6 +9,7 @@ const { } = new SeleniumHelper(); const uri = path.resolve(__dirname, '../../build/index.html?tutorial=all'); +const uriPrefix = path.resolve(__dirname, '../../build/index.html?tutorial='); let driver; @@ -24,9 +25,14 @@ describe('Working with shortcut to Tutorials library', () => { test('opens with the Tutorial Library showing', async () => { await loadUri(uri); // make sure there is a tutorial visible that doesn't have a shortcut - await clickText('Switch costume'); + await clickText('Make It Spin'); await findByXpath('//div[contains(@class, "step-video")]'); }); + test('can open hidden tutorials', async () => { + await loadUri(`${uriPrefix}whatsnew`); + // should open the tutorial video immediately + await findByXpath('//div[contains(@class, "step-video")]'); + }); // @todo navigating cards, etc. }); diff --git a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap index 93ebc2cf6..38ddf76c4 100644 --- a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap +++ b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap @@ -211,24 +211,17 @@ exports[`Sound Editor Component matches snapshot 1`] = `
+
+ /> +
-
- -
5
- + > +
+ +
+
@@ -49,6 +45,18 @@ exports[`SpriteSelectorItemComponent matches snapshot when given a number and de 480 x 360
+
+ +