From b54d104e9cd4af5d2baf9f27a117565318e5930f Mon Sep 17 00:00:00 2001 From: mark <94125865+mmmmaaaaarrrrrrkkkkkkkk@users.noreply.github.com> Date: Mon, 16 Jun 2025 21:22:39 -0500 Subject: [PATCH 1/3] animations!! --- src/components/button/button.css | 12 ++ src/components/button/button.jsx | 1 + src/components/library-item/library-item.css | 10 + src/components/library/library.jsx | 1 + src/components/menu/menu.css | 10 + src/components/menu/menu.jsx | 48 +++-- src/components/modal/modal.css | 40 ++++ src/components/modal/modal.jsx | 181 +++++++++------- .../sprite-selector-item.css | 6 + .../sprite-selector-item.jsx | 199 ++++++++++-------- src/containers/extension-library.jsx | 1 + src/containers/sprite-selector-item.jsx | 2 +- 12 files changed, 335 insertions(+), 176 deletions(-) diff --git a/src/components/button/button.css b/src/components/button/button.css index 30f000a7a36..c537b66a9ac 100644 --- a/src/components/button/button.css +++ b/src/components/button/button.css @@ -12,6 +12,18 @@ user-select: none; } +.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..e9d29c1b0cd 100644 --- a/src/components/button/button.jsx +++ b/src/components/button/button.jsx @@ -34,6 +34,7 @@ const ButtonComponent = ({ {/* todo: translation support? diff --git a/src/components/menu/menu.css b/src/components/menu/menu.css index 4c7621e96cd..32ab567dbb1 100644 --- a/src/components/menu/menu.css +++ b/src/components/menu/menu.css @@ -13,7 +13,17 @@ 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); } + +.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..ea9a8de8add 100644 --- a/src/components/menu/menu.jsx +++ b/src/components/menu/menu.jsx @@ -1,29 +1,43 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; -import React from 'react'; - +import React, {useState, useEffect, useRef} from 'react'; import styles from './menu.css'; +const animIn = 0; // ms, you could add a delay but it doesn't feel right + const MenuComponent = ({ className = '', children, componentRef, place = '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, diff --git a/src/components/modal/modal.css b/src/components/modal/modal.css index 89e6995e86c..87734bdfc45 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,39 @@ 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); +} + + +.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..2081366c418 100644 --- a/src/components/modal/modal.jsx +++ b/src/components/modal/modal.jsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; 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 +13,123 @@ import helpIcon from '../../lib/assets/icon--help.svg'; import styles from './modal.css'; -const ModalComponent = props => ( - - { + 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, diff --git a/src/components/sprite-selector-item/sprite-selector-item.css b/src/components/sprite-selector-item/sprite-selector-item.css index 77c34e8b7a3..641f969158b 100644 --- a/src/components/sprite-selector-item/sprite-selector-item.css +++ b/src/components/sprite-selector-item/sprite-selector-item.css @@ -20,6 +20,12 @@ 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); +} + +.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..a2086172f09 100644 --- a/src/components/sprite-selector-item/sprite-selector-item.jsx +++ b/src/components/sprite-selector-item/sprite-selector-item.jsx @@ -1,100 +1,129 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; -import React from 'react'; +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 => ( - - {typeof props.number === 'undefined' ? null : ( -
{props.number}
- )} - {props.costumeURL ? ( -
-
- +const animOut = 150; // ms + +const SpriteSelectorItem = props => { + const [visible, setVisible] = useState(true); // provides a clear way to check visibility + const waitOut = useRef(null); + const onReqCloseRef = useRef(props.onDeleteButtonClick); + + useEffect(() => { + onReqCloseRef.current = props.onDeleteButtonClick; + }, [props.onDeleteButtonClick]); + + 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 onDelete = useCallback(() => { + setVisible(false); // animate out + waitOut.current = setTimeout(() => { + console.log('SpriteSelectorItem: onDelete'); + if (onReqCloseRef.current) onReqCloseRef.current(); // delete sprite after animating out + }, animOut); + }, []); + + return ( + + {typeof props.number === 'undefined' ? null : ( +
{props.number}
+ )} + {props.costumeURL ? ( +
+
+ +
-
- ) : null} -
-
{props.name}
- {props.details ? ( -
{props.details}
) : null} -
- {(props.selected && props.onDeleteButtonClick) ? ( - - ) : null } - {props.onDuplicateButtonClick || props.onDeleteButtonClick || props.onExportButtonClick ? ( - - {props.onDuplicateButtonClick ? ( - - - +
+
{props.name}
+ {props.details ? ( +
{props.details}
) : null} - {props.onExportButtonClick ? ( - - - - ) : null } - {props.onRenameButtonClick ? ( - - - - ) : null} - {props.onDeleteButtonClick ? ( - - - - ) : null } - - ) : null} - -); +
+ {(props.selected && props.onDeleteButtonClick) ? ( + + ) : null} + {props.onDuplicateButtonClick || props.onDeleteButtonClick || props.onExportButtonClick ? ( + + {props.onDuplicateButtonClick ? ( + + + + ) : null} + {props.onExportButtonClick ? ( + + + + ) : null} + {props.onRenameButtonClick ? ( + + + + ) : null} + {props.onDeleteButtonClick ? ( + + + + ) : null} + + ) : null} + + ); +} SpriteSelectorItem.propTypes = { className: PropTypes.string, diff --git a/src/containers/extension-library.jsx b/src/containers/extension-library.jsx index 4ffd582d0c5..2d4b936b5c9 100644 --- a/src/containers/extension-library.jsx +++ b/src/containers/extension-library.jsx @@ -214,6 +214,7 @@ class ExtensionLibrary extends React.PureComponent { id="extensionLibrary" actor="ExtensionLibrary" header={"Extensions"} + kind="extension" title={this.props.intl.formatMessage(messages.extensionTitle)} visible={this.props.visible} onItemSelected={this.handleItemSelect} diff --git a/src/containers/sprite-selector-item.jsx b/src/containers/sprite-selector-item.jsx index 1d601b8cebe..dd45d64a213 100644 --- a/src/containers/sprite-selector-item.jsx +++ b/src/containers/sprite-selector-item.jsx @@ -91,7 +91,7 @@ class SpriteSelectorItem extends React.PureComponent { } } handleDelete (e) { - e.stopPropagation(); // To prevent from bubbling back to handleClick + if (e) e.stopPropagation(); // To prevent from bubbling back to handleClick this.props.onDeleteButtonClick(this.props.id); } handleDuplicate (e) { From 59406e2bcfa6c6428ef0ac65c312e6a810b8f7ac Mon Sep 17 00:00:00 2001 From: mark <94125865+mmmmaaaaarrrrrrkkkkkkkk@users.noreply.github.com> Date: Tue, 17 Jun 2025 13:01:58 -0500 Subject: [PATCH 2/3] animations 2!!! --- src/addons/addons.js | 1 + .../addons/animations/_manifest_entry.js | 47 +++++++++++++++++++ .../addons/animations/_runtime_entry.js | 4 ++ src/addons/addons/animations/userscript.js | 18 +++++++ src/addons/generated/addon-entries.js | 1 + src/addons/generated/addon-manifests.js | 2 + src/components/button/button.css | 4 ++ src/components/button/button.jsx | 18 +++++-- src/components/library-item/library-item.css | 4 ++ src/components/library-item/library-item.jsx | 15 +++++- src/components/menu/menu.css | 4 ++ src/components/menu/menu.jsx | 23 ++++++--- src/components/modal/modal.css | 4 ++ src/components/modal/modal.jsx | 22 +++++++-- .../sprite-selector-item.css | 4 ++ .../sprite-selector-item.jsx | 18 +++++-- src/reducers/addon-util.js | 16 ++++++- 17 files changed, 186 insertions(+), 19 deletions(-) create mode 100644 src/addons/addons/animations/_manifest_entry.js create mode 100644 src/addons/addons/animations/_runtime_entry.js create mode 100644 src/addons/addons/animations/userscript.js 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..8129f612641 --- /dev/null +++ b/src/addons/addons/animations/_manifest_entry.js @@ -0,0 +1,47 @@ +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": "none", + "name": "No animations" + }, + { + "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..72decace34b --- /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') || "default" + }); + } + function resetSettings() { + ReduxStore.dispatch({ + type: 'scratch-gui/addon-util/SET_EDITOR_ANIM_PREF', + animPref: "default" + }); + } + 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 c537b66a9ac..4cfca26aaf6 100644 --- a/src/components/button/button.css +++ b/src/components/button/button.css @@ -12,6 +12,10 @@ 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); } diff --git a/src/components/button/button.jsx b/src/components/button/button.jsx index e9d29c1b0cd..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 80560e21e48..0ac1ab6f1ff 100644 --- a/src/components/library-item/library-item.css +++ b/src/components/library-item/library-item.css @@ -24,6 +24,10 @@ 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); } 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/menu/menu.css b/src/components/menu/menu.css index 32ab567dbb1..40ec29c1ea2 100644 --- a/src/components/menu/menu.css +++ b/src/components/menu/menu.css @@ -19,6 +19,10 @@ 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); diff --git a/src/components/menu/menu.jsx b/src/components/menu/menu.jsx index ea9a8de8add..afd33a94893 100644 --- a/src/components/menu/menu.jsx +++ b/src/components/menu/menu.jsx @@ -1,16 +1,20 @@ import classNames from 'classnames'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import React, {useState, useEffect, useRef} from 'react'; import styles from './menu.css'; -const animIn = 0; // ms, you could add a delay but it doesn't feel right + 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); @@ -30,6 +34,7 @@ const MenuComponent = ({ [styles.left]: place === 'left', [styles.right]: place === 'right', [styles.menuVisible]: visible, + [styles.noAnimation]: animPref == 'none' || prefersReducedMotion } )} ref={componentRef} @@ -91,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 87734bdfc45..9535cb9263d 100644 --- a/src/components/modal/modal.css +++ b/src/components/modal/modal.css @@ -45,6 +45,10 @@ } +.no-animation { + transition: opacity 0s ease, transform 0s ease !important; +} + .modal-fs-visible { opacity: 1; transform: scale(1) rotateX(0deg); diff --git a/src/components/modal/modal.jsx b/src/components/modal/modal.jsx index 2081366c418..67d5bceb4a1 100644 --- a/src/components/modal/modal.jsx +++ b/src/components/modal/modal.jsx @@ -1,4 +1,5 @@ import classNames from 'classnames'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import React, {useState, useEffect, useRef, useCallback} from 'react'; import ReactModal from 'react-modal'; @@ -13,9 +14,11 @@ import helpIcon from '../../lib/assets/icon--help.svg'; import styles from './modal.css'; -const animOut = 200; // ms + 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); @@ -50,13 +53,15 @@ const ModalComponent = props => { [styles.fullScreen]: props.fullScreen, [styles.modalFsVisible]: visible && props.fullScreen, [styles.modalVisible]: visible && !props.fullScreen, - [styles.extModal]: props.kind == 'extension' + [styles.extModal]: props.kind == 'extension', + [styles.noAnimation]: (props.animPref != 'intense' && props.fullScreen) || props.animPref == 'none' || prefersReducedMotion } )} contentLabel={props.contentLabel} overlayClassName={classNames(styles.modalOverlay, { [styles.scrollable]: props.scrollable, [styles.modalOverlayVisible]: visible, + [styles.noAnimation]: (props.animPref != 'intense' && props.fullScreen) || props.animPref == 'none' || prefersReducedMotion })} > { + 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 641f969158b..2c9440edf22 100644 --- a/src/components/sprite-selector-item/sprite-selector-item.css +++ b/src/components/sprite-selector-item/sprite-selector-item.css @@ -23,6 +23,10 @@ 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); diff --git a/src/components/sprite-selector-item/sprite-selector-item.jsx b/src/components/sprite-selector-item/sprite-selector-item.jsx index a2086172f09..52e82202de2 100644 --- a/src/components/sprite-selector-item/sprite-selector-item.jsx +++ b/src/components/sprite-selector-item/sprite-selector-item.jsx @@ -1,5 +1,6 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; import React, {useState, useEffect, useRef, useCallback} from 'react'; import DeleteButton from '../delete-button/delete-button.jsx'; @@ -11,9 +12,11 @@ import { FormattedMessage } from 'react-intl'; // react-contextmenu requires unique id to match trigger and context menu let contextMenuId = 0; -const animOut = 150; // ms + const SpriteSelectorItem = props => { + const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + const animOut = props.animPref == "none" ? 0 : 150; // ms const [visible, setVisible] = useState(true); // provides a clear way to check visibility const waitOut = useRef(null); const onReqCloseRef = useRef(props.onDeleteButtonClick); @@ -43,7 +46,8 @@ const SpriteSelectorItem = props => { attributes={{ className: classNames(props.className, styles.spriteSelectorItem, { [styles.isSelected]: props.selected, - [styles.deleting]: !visible + [styles.deleting]: !visible, + [styles.noAnimation]: props.animPref == 'none' || prefersReducedMotion }), onClick: props.onClick, onMouseEnter: props.onMouseEnter, @@ -145,4 +149,12 @@ SpriteSelectorItem.propTypes = { selected: PropTypes.bool.isRequired }; -export default SpriteSelectorItem; +const mapStateToProps = (state) => { + return { + animPref: state.scratchGui.addonUtil.editorAnimPref, + }; +}; + +export default connect( + mapStateToProps +)(SpriteSelectorItem); diff --git a/src/reducers/addon-util.js b/src/reducers/addon-util.js index 9d5c2df08a4..376cc20be65 100644 --- a/src/reducers/addon-util.js +++ b/src/reducers/addon-util.js @@ -1,7 +1,9 @@ const SET_SOUND_EDITOR_WAVEFORM_CHUNK_SIZE = 'scratch-gui/addon-util/SET_SOUND_EDITOR_WAVEFORM_CHUNK_SIZE'; +const SET_EDITOR_ANIM_PREF = 'scratch-gui/addon-util/SET_EDITOR_ANIM_PREF'; const initialState = { soundEditorWaveformChunkSize: 1024, + editorAnimPref: 'default' }; const reducer = function (state, action) { @@ -11,6 +13,10 @@ const reducer = function (state, action) { return { soundEditorWaveformChunkSize: action.chunkSize }; + case SET_EDITOR_ANIM_PREF: + return { + editorAnimPref: action.animPref + }; default: return state; } @@ -23,8 +29,16 @@ const setSoundEditorWaveformChunkSize = function (chunkSize) { }; }; +const setEditorAnimPref = function (preference) { + return { + type: SET_EDITOR_ANIM_PREF, + animPref: preference + }; +}; + export { reducer as default, initialState as addonUtilInitialState, - setSoundEditorWaveformChunkSize + setSoundEditorWaveformChunkSize, + setEditorAnimPref }; \ No newline at end of file From 9c6430feef5a47f154498ff325b20fab8e865407 Mon Sep 17 00:00:00 2001 From: mark <94125865+mmmmaaaaarrrrrrkkkkkkkk@users.noreply.github.com> Date: Wed, 18 Jun 2025 22:06:00 -0500 Subject: [PATCH 3/3] hwehjkdhkjahsdj turning off now sets to none maybe I think hopefully --- src/addons/addons/animations/_manifest_entry.js | 4 ---- src/addons/addons/animations/userscript.js | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/addons/addons/animations/_manifest_entry.js b/src/addons/addons/animations/_manifest_entry.js index 8129f612641..f0b16d0f6f2 100644 --- a/src/addons/addons/animations/_manifest_entry.js +++ b/src/addons/addons/animations/_manifest_entry.js @@ -24,10 +24,6 @@ const manifest = { "id": "intensity", "type": "select", "potentialValues": [ - { - "id": "none", - "name": "No animations" - }, { "id": "default", "name": "Moderate animations" diff --git a/src/addons/addons/animations/userscript.js b/src/addons/addons/animations/userscript.js index 72decace34b..43236b70e9f 100644 --- a/src/addons/addons/animations/userscript.js +++ b/src/addons/addons/animations/userscript.js @@ -2,13 +2,13 @@ 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') || "default" + animPref: addon.settings.get('intensity') || "none" }); } function resetSettings() { ReduxStore.dispatch({ type: 'scratch-gui/addon-util/SET_EDITOR_ANIM_PREF', - animPref: "default" + animPref: "none" }); } addon.self.addEventListener("reenabled", applySettings);