diff --git a/src/addons/addons.js b/src/addons/addons.js
index 279fad00840..72e20dfb47f 100644
--- a/src/addons/addons.js
+++ b/src/addons/addons.js
@@ -77,6 +77,7 @@ const addons = [
];
const newAddons = [
+ "animations",
"paint-gradient-maker",
"toolbox-full-blocks-on-hover",
"waveform-chunk-size",
diff --git a/src/addons/addons/animations/_manifest_entry.js b/src/addons/addons/animations/_manifest_entry.js
new file mode 100644
index 00000000000..f0b16d0f6f2
--- /dev/null
+++ b/src/addons/addons/animations/_manifest_entry.js
@@ -0,0 +1,43 @@
+const manifest = {
+ "name": "Animation Types",
+ "description": "Change the intensity of animations that appear in the editor. Some moderate animations come with the editor by default.",
+ "credits": [
+ {
+ "name": "Reflow"
+ }
+ ],
+ "userscripts": [
+ {
+ "url": "userscript.js"
+ }
+ ],
+ "info": [
+ {
+ "text": "If you enabled a reduced motion setting on your device, these settings will not apply.",
+ "id": "reduced-motion"
+ }
+ ],
+ "settings": [
+ {
+ "dynamic": true,
+ "name": "Intensity",
+ "id": "intensity",
+ "type": "select",
+ "potentialValues": [
+ {
+ "id": "default",
+ "name": "Moderate animations"
+ },
+ {
+ "id": "intense",
+ "name": "Shower me in animations"
+ }
+ ],
+ "default": "default"
+ }
+ ],
+ "tags": ["editor", "new"],
+ "enabledByDefault": true,
+ "dynamicDisable": true
+};
+export default manifest;
diff --git a/src/addons/addons/animations/_runtime_entry.js b/src/addons/addons/animations/_runtime_entry.js
new file mode 100644
index 00000000000..675d8864ebc
--- /dev/null
+++ b/src/addons/addons/animations/_runtime_entry.js
@@ -0,0 +1,4 @@
+import _js from "./userscript.js";
+export const resources = {
+ "userscript.js": _js,
+};
diff --git a/src/addons/addons/animations/userscript.js b/src/addons/addons/animations/userscript.js
new file mode 100644
index 00000000000..43236b70e9f
--- /dev/null
+++ b/src/addons/addons/animations/userscript.js
@@ -0,0 +1,18 @@
+export default async function ({ addon, console, msg }) {
+ function applySettings() {
+ ReduxStore.dispatch({
+ type: 'scratch-gui/addon-util/SET_EDITOR_ANIM_PREF',
+ animPref: addon.settings.get('intensity') || "none"
+ });
+ }
+ function resetSettings() {
+ ReduxStore.dispatch({
+ type: 'scratch-gui/addon-util/SET_EDITOR_ANIM_PREF',
+ animPref: "none"
+ });
+ }
+ addon.self.addEventListener("reenabled", applySettings);
+ addon.self.addEventListener("disabled", resetSettings);
+ addon.settings.addEventListener("change", applySettings);
+ applySettings();
+}
diff --git a/src/addons/generated/addon-entries.js b/src/addons/generated/addon-entries.js
index ecf4eaace66..2954e59b1b5 100644
--- a/src/addons/generated/addon-entries.js
+++ b/src/addons/generated/addon-entries.js
@@ -73,4 +73,5 @@ export default {
"tw-disable-cloud-variables": () => import(/* webpackChunkName: "addon-entry-tw-disable-cloud-variables" */ "../addons/tw-disable-cloud-variables/_runtime_entry.js"),
"vol-slider": () => import(/* webpackChunkName: "addon-entry-vol-slider" */ "../addons/vol-slider/_runtime_entry.js"),
"waveform-chunk-size": () => import(/* webpackChunkName: "addon-default-entry" */ "../addons/waveform-chunk-size/_runtime_entry.js"),
+ "animations": () => import(/* webpackChunkName: "addon-entry-animations" */ "../addons/animations/_runtime_entry.js"),
};
diff --git a/src/addons/generated/addon-manifests.js b/src/addons/generated/addon-manifests.js
index 5a7b2d0fbb3..3ab24504072 100644
--- a/src/addons/generated/addon-manifests.js
+++ b/src/addons/generated/addon-manifests.js
@@ -71,8 +71,10 @@ import _tw_straighten_comments from "../addons/tw-straighten-comments/_manifest_
import _tw_remove_feedback from "../addons/tw-remove-feedback/_manifest_entry.js";
import _tw_remove_backpack from "../addons/tw-remove-backpack/_manifest_entry.js";
import _tw_disable_cloud_variables from "../addons/tw-disable-cloud-variables/_manifest_entry.js";
+import _animations from "../addons/animations/_manifest_entry.js";
export default {
+ "animations": _animations,
"cat-blocks": _cat_blocks,
"editor-devtools": _editor_devtools,
"find-bar": _find_bar,
diff --git a/src/components/button/button.css b/src/components/button/button.css
index 30f000a7a36..4cfca26aaf6 100644
--- a/src/components/button/button.css
+++ b/src/components/button/button.css
@@ -12,6 +12,22 @@
user-select: none;
}
+.no-animation {
+ transition: transform 0s ease !important;
+}
+
+.button {
+ transition: transform 0.15s cubic-bezier(0.63, 0.32, 0.08, 0.95);
+}
+
+.button:hover {
+ transform: scale(1.02);
+}
+
+.button:active {
+ transform: scale(0.95);
+}
+
.icon {
height: 1.5rem;
}
diff --git a/src/components/button/button.jsx b/src/components/button/button.jsx
index 2e177f0ef67..3ad372770ab 100644
--- a/src/components/button/button.jsx
+++ b/src/components/button/button.jsx
@@ -1,4 +1,5 @@
import classNames from 'classnames';
+import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import React from 'react';
@@ -13,16 +14,19 @@ const ButtonComponent = ({
iconHeight,
onClick,
children,
+ animPref,
...props
}) => {
-
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (disabled) {
onClick = function () {};
}
const icon = iconSrc && (
{
+ return {
+ animPref: state.scratchGui.addonUtil.editorAnimPref,
+ };
+};
+
+export default connect(
+ mapStateToProps
+)(ButtonComponent);
diff --git a/src/components/library-item/library-item.css b/src/components/library-item/library-item.css
index 606acd8dbf0..0ac1ab6f1ff 100644
--- a/src/components/library-item/library-item.css
+++ b/src/components/library-item/library-item.css
@@ -21,7 +21,21 @@
border-radius: $space;
text-align: center;
cursor: pointer;
+ transition: transform 0.15s cubic-bezier(0.63, 0.32, 0.08, 0.95), border-color 0.15s cubic-bezier(0.63, 0.32, 0.08, 0.95);
}
+
+.no-animation {
+ transition: transform 0s ease, border-color 0s ease !important;
+}
+
+.library-item:hover {
+ transform: scale(1.02);
+}
+
+.library-item:active {
+ transform: scale(0.98);
+}
+
[theme="dark"] .library-item {
background: $ui-primary;
}
diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx
index 70347080154..aa591f7c8ed 100644
--- a/src/components/library-item/library-item.jsx
+++ b/src/components/library-item/library-item.jsx
@@ -1,5 +1,6 @@
import {FormattedMessage, intlShape, defineMessages} from 'react-intl';
import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
import React from 'react';
import Box from '../box/box.jsx';
@@ -32,13 +33,15 @@ const getMSFormatted = (ms) => {
/* eslint-disable react/prefer-stateless-function */
class LibraryItemComponent extends React.PureComponent {
render() {
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
return this.props.featured ? (
{
+ return {
+ animPref: state.scratchGui.addonUtil.editorAnimPref,
+ };
+};
+
+export default connect(
+ mapStateToProps
+)(LibraryItemComponent);
diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx
index f3f44344531..d5c3e183c0f 100644
--- a/src/components/library/library.jsx
+++ b/src/components/library/library.jsx
@@ -306,6 +306,7 @@ class LibraryComponent extends React.Component {
contentLabel={this.props.title}
id={this.props.id}
onRequestClose={this.handleClose}
+ kind={this.props.kind}
>
{/*
todo: translation support?
diff --git a/src/components/menu/menu.css b/src/components/menu/menu.css
index 4c7621e96cd..40ec29c1ea2 100644
--- a/src/components/menu/menu.css
+++ b/src/components/menu/menu.css
@@ -13,7 +13,21 @@
overflow: visible;
color: $ui-white;
box-shadow: 0 8px 8px 0 $ui-black-transparent-default;
+ transform-origin: top left;
+ opacity: 0;
+ transform: scale(0.6);
+ transition: opacity 0.15s cubic-bezier(0.63, 0.32, 0.08, 0.95), transform 0.15s cubic-bezier(0.63, 0.32, 0.08, 0.95);
}
+
+.no-animation {
+ transition: opacity 0s ease, transform 0s ease !important;
+}
+
+.menu-visible {
+ opacity: 1;
+ transform: scale(1);
+}
+
[theme="dark"] .menu {
background-color: $motion-primary-dark;
}
diff --git a/src/components/menu/menu.jsx b/src/components/menu/menu.jsx
index 9f14ba52b2d..afd33a94893 100644
--- a/src/components/menu/menu.jsx
+++ b/src/components/menu/menu.jsx
@@ -1,29 +1,48 @@
import classNames from 'classnames';
+import { connect } from 'react-redux';
import PropTypes from 'prop-types';
-import React from 'react';
-
+import React, {useState, useEffect, useRef} from 'react';
import styles from './menu.css';
+
+
const MenuComponent = ({
className = '',
children,
componentRef,
+ animPref,
place = 'right'
-}) => (
-
-);
+}, props) => {
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ const animIn = animPref == "none" ? 0 : 0; // ms, you could add a delay but it doesn't feel right
+ const [visible, setVisible] = useState(false); // provides a clear way to check visibility
+ const waitOut = useRef(null);
+
+ useEffect(() => {
+ const waitIn = setTimeout(() => setVisible(true), animIn);
+ return () => {
+ clearTimeout(waitIn);
+ if (waitOut.current) clearTimeout(waitOut.current);
+ };
+ }, []);
+ return (
+
+ )
+};
MenuComponent.propTypes = {
children: PropTypes.node,
@@ -77,8 +96,14 @@ MenuSection.propTypes = {
children: PropTypes.node
};
-export {
- MenuComponent as default,
- MenuItem,
- MenuSection
+export { MenuItem, MenuSection };
+
+const mapStateToProps = (state) => {
+ return {
+ animPref: state.scratchGui.addonUtil.editorAnimPref,
+ };
};
+
+export default connect(
+ mapStateToProps
+)(MenuComponent);
diff --git a/src/components/modal/modal.css b/src/components/modal/modal.css
index 89e6995e86c..9535cb9263d 100644
--- a/src/components/modal/modal.css
+++ b/src/components/modal/modal.css
@@ -10,7 +10,14 @@
bottom: 0;
z-index: $z-index-modal;
background-color: $ui-modal-overlay;
+ opacity: 0;
+ transition: opacity 0.15s cubic-bezier(0.63, 0.32, 0.08, 0.95);
}
+
+.modal-overlay-visible {
+ opacity: 1;
+}
+
.scrollable {
overflow: auto;
}
@@ -19,6 +26,43 @@
box-sizing: border-box;
}
+.full-screen {
+ opacity: 0;
+ transform: scale(0);
+ transition: opacity 0.5s cubic-bezier(0.63, 0.32, 0.08, 0.95), transform 0.5s cubic-bezier(0.63, 0.32, 0.08, 0.95);
+ -webkit-perspective: 240px;
+ perspective: 240px;
+}
+
+.modal-container {
+ opacity: 0;
+ transform: scale(0.7) rotateX(-45deg);
+ transition: opacity 0.15s cubic-bezier(0.63, 0.32, 0.08, 0.95), transform 0.2s cubic-bezier(0.63, 0.32, 0.08, 0.95);
+ -webkit-perspective: 240px;
+ perspective: 240px;
+ max-width: max(60%, 750px);
+ width: max(60%, 750px);
+}
+
+
+.no-animation {
+ transition: opacity 0s ease, transform 0s ease !important;
+}
+
+.modal-fs-visible {
+ opacity: 1;
+ transform: scale(1) rotateX(0deg);
+}
+
+.modal-visible {
+ opacity: 1;
+ transform: scale(1) rotateX(0deg);
+}
+
+.ext-modal {
+ transform-origin: bottom left;
+}
+
.modal-content {
margin: 100px auto;
outline: none;
diff --git a/src/components/modal/modal.jsx b/src/components/modal/modal.jsx
index 66e9f22611f..67d5bceb4a1 100644
--- a/src/components/modal/modal.jsx
+++ b/src/components/modal/modal.jsx
@@ -1,6 +1,7 @@
import classNames from 'classnames';
+import { connect } from 'react-redux';
import PropTypes from 'prop-types';
-import React from 'react';
+import React, {useState, useEffect, useRef, useCallback} from 'react';
import ReactModal from 'react-modal';
import {FormattedMessage} from 'react-intl';
@@ -13,88 +14,127 @@ import helpIcon from '../../lib/assets/icon--help.svg';
import styles from './modal.css';
-const ModalComponent = props => (
-
- {
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ const animOut = props.animPref == "none" ? 0 : 200; // ms
+ const [visible, setVisible] = useState(false); // provides a clear way to check visibility
+ const waitOut = useRef(null);
+ const onReqCloseRef = useRef(props.onRequestClose);
+
+ useEffect(() => {
+ onReqCloseRef.current = props.onRequestClose;
+ }, [props.onRequestClose]);
+
+ useEffect(() => {
+ const waitIn = setTimeout(() => setVisible(true), 0); // you could add an "in" delay here but it just doesn't feel right
+ return () => {
+ clearTimeout(waitIn);
+ if (waitOut.current) clearTimeout(waitOut.current);
+ };
+ }, []);
+
+ const closeThisModal = useCallback(() => {
+ setVisible(false); // animate out
+ waitOut.current = setTimeout(() => {
+ if (onReqCloseRef.current) onReqCloseRef.current(); // close window after animating out
+ }, animOut);
+ }, []);
+
+ return (
+
-
- {props.onHelp ? (
+
+
+ {props.onHelp ? (
+
+
+
+ ) : null}
-
+ ) : null}
+ {props.contentLabel}
- ) : null}
-
- {props.headerImage ? (
-

- ) : null}
- {props.contentLabel}
-
-
- {props.fullScreen ? (
-
- ) : (
-
- )}
+ )}
+
-
- {props.children}
-
-
-);
+ {props.children}
+
+
+ )
+};
ModalComponent.propTypes = {
children: PropTypes.node,
@@ -109,7 +149,16 @@ ModalComponent.propTypes = {
isRtl: PropTypes.bool,
onHelp: PropTypes.func,
onRequestClose: PropTypes.func,
- scrollable: PropTypes.bool
+ scrollable: PropTypes.bool,
+ animPref: PropTypes.string
+};
+
+const mapStateToProps = (state) => {
+ return {
+ animPref: state.scratchGui.addonUtil.editorAnimPref,
+ };
};
-export default ModalComponent;
+export default connect(
+ mapStateToProps
+)(ModalComponent);
diff --git a/src/components/sprite-selector-item/sprite-selector-item.css b/src/components/sprite-selector-item/sprite-selector-item.css
index 77c34e8b7a3..2c9440edf22 100644
--- a/src/components/sprite-selector-item/sprite-selector-item.css
+++ b/src/components/sprite-selector-item/sprite-selector-item.css
@@ -20,6 +20,16 @@
cursor: pointer;
user-select: none;
+ transition: transform 0.15s cubic-bezier(0.63, 0.32, 0.08, 0.95), opacity 0.15s cubic-bezier(0.63, 0.32, 0.08, 0.95);
+}
+
+.no-animation {
+ transition: opacity 0s ease, transform 0s ease !important;
+}
+
+.deleting {
+ opacity: 0;
+ transform: scale(0);
}
.sprite-selector-item.is-selected {
diff --git a/src/components/sprite-selector-item/sprite-selector-item.jsx b/src/components/sprite-selector-item/sprite-selector-item.jsx
index 745745d400d..52e82202de2 100644
--- a/src/components/sprite-selector-item/sprite-selector-item.jsx
+++ b/src/components/sprite-selector-item/sprite-selector-item.jsx
@@ -1,100 +1,133 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
-import React from 'react';
+import { connect } from 'react-redux';
+import React, {useState, useEffect, useRef, useCallback} from 'react';
import DeleteButton from '../delete-button/delete-button.jsx';
import styles from './sprite-selector-item.css';
-import {ContextMenuTrigger} from 'react-contextmenu';
-import {DangerousMenuItem, ContextMenu, MenuItem} from '../context-menu/context-menu.jsx';
-import {FormattedMessage} from 'react-intl';
+import { ContextMenuTrigger } from 'react-contextmenu';
+import { DangerousMenuItem, ContextMenu, MenuItem } from '../context-menu/context-menu.jsx';
+import { FormattedMessage } from 'react-intl';
// react-contextmenu requires unique id to match trigger and context menu
let contextMenuId = 0;
-const SpriteSelectorItem = props => (
-