From d05b56a595366c3fc8affddbffa7e840dabe764a Mon Sep 17 00:00:00 2001 From: Karishma Chadha <kchadha@media.mit.edu> Date: Wed, 10 Oct 2018 18:35:45 -0400 Subject: [PATCH] Add sprite/stage watermark to the blocks workspace. --- src/components/gui/gui.css | 14 +++++ src/components/gui/gui.jsx | 4 ++ src/components/watermark/watermark.css | 9 +++ src/components/watermark/watermark.jsx | 17 +++++ src/containers/sprite-selector-item.jsx | 46 +++++--------- src/containers/watermark.jsx | 83 +++++++++++++++++++++++++ src/lib/get-costume-url.js | 28 +++++++++ 7 files changed, 169 insertions(+), 32 deletions(-) create mode 100644 src/components/watermark/watermark.css create mode 100644 src/components/watermark/watermark.jsx create mode 100644 src/containers/watermark.jsx create mode 100644 src/lib/get-costume-url.js diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index a10b08b60..6118b7997 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -279,6 +279,20 @@ $fade-out-distance: 15px; margin-top: 0; } +/* Sprite Selection Watermark */ +.watermark { + position: absolute; + top: 1.25rem; +} + +[dir="ltr"] .watermark { + right: 1.25rem; +} + +[dir="rtl"] .watermark { + left: 1.25rem; +} + /* Menu */ .menu-bar-position { diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index d39e22833..b37dcbd79 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -20,6 +20,7 @@ import Box from '../box/box.jsx'; import MenuBar from '../menu-bar/menu-bar.jsx'; import CostumeLibrary from '../../containers/costume-library.jsx'; import BackdropLibrary from '../../containers/backdrop-library.jsx'; +import Watermark from '../../containers/watermark.jsx'; import Backpack from '../../containers/backpack.jsx'; import PreviewModal from '../../containers/preview-modal.jsx'; @@ -261,6 +262,9 @@ const GUIComponent = props => { /> </button> </Box> + <Box className={styles.watermark}> + <Watermark /> + </Box> </TabPanel> <TabPanel className={tabClassNames.tabPanel}> {costumesTabVisible ? <CostumeTab vm={vm} /> : null} diff --git a/src/components/watermark/watermark.css b/src/components/watermark/watermark.css new file mode 100644 index 000000000..f02e3242c --- /dev/null +++ b/src/components/watermark/watermark.css @@ -0,0 +1,9 @@ + +.sprite-image { + margin: auto; + user-select: none; + max-width: 48px; + max-height: 48px; + opacity: 0.35; + pointer-events: none; +} diff --git a/src/components/watermark/watermark.jsx b/src/components/watermark/watermark.jsx new file mode 100644 index 000000000..d3f5b36e2 --- /dev/null +++ b/src/components/watermark/watermark.jsx @@ -0,0 +1,17 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import styles from './watermark.css'; + +const Watermark = props => ( + <img + className={styles.spriteImage} + src={props.costumeURL} + /> +); + +Watermark.propTypes = { + costumeURL: PropTypes.string +}; + +export default Watermark; diff --git a/src/containers/sprite-selector-item.jsx b/src/containers/sprite-selector-item.jsx index 9b154c59b..c33aa41e7 100644 --- a/src/containers/sprite-selector-item.jsx +++ b/src/containers/sprite-selector-item.jsx @@ -7,19 +7,17 @@ import {setHoveredSprite} from '../reducers/hovered-target'; import {updateAssetDrag} from '../reducers/asset-drag'; import {getEventXY} from '../lib/touch-utils'; import VM from 'scratch-vm'; -import {SVGRenderer} from 'scratch-svg-renderer'; +import getCostumeUrl from '../lib/get-costume-url'; import SpriteSelectorItemComponent from '../components/sprite-selector-item/sprite-selector-item.jsx'; const dragThreshold = 3; // Same as the block drag threshold -// Contains 'font-family', but doesn't only contain 'font-family="none"' -const HAS_FONT_REGEXP = 'font-family(?!="none")'; class SpriteSelectorItem extends React.Component { constructor (props) { super(props); bindAll(this, [ - 'getCostumeUrl', + 'getCostumeData', 'handleClick', 'handleDelete', 'handleDuplicate', @@ -30,8 +28,8 @@ class SpriteSelectorItem extends React.Component { 'handleMouseMove', 'handleMouseUp' ]); - this.svgRenderer = new SVGRenderer(); - // Asset ID of the SVG currently in SVGRenderer + + // Asset ID of the current decoded costume this.decodedAssetId = null; } shouldComponentUpdate (nextProps) { @@ -44,32 +42,19 @@ class SpriteSelectorItem extends React.Component { } return false; } - getCostumeUrl () { + getCostumeData () { if (this.props.costumeURL) return this.props.costumeURL; if (!this.props.assetId) return null; - const storage = this.props.vm.runtime.storage; - const asset = storage.get(this.props.assetId); - // If the SVG refers to fonts, they must be inlined in order to display correctly in the img tag. - // Avoid parsing the SVG when possible, since it's expensive. - if (asset.assetType === storage.AssetType.ImageVector) { - // If the asset ID has not changed, no need to re-parse - if (this.decodedAssetId === this.props.assetId) { - // @todo consider caching more than one URL. - return this.cachedUrl; - } - this.decodedAssetId = this.props.assetId; - const svgString = this.props.vm.runtime.storage.get(this.props.assetId).decodeText(); - if (svgString.match(HAS_FONT_REGEXP)) { - this.svgRenderer.loadString(svgString); - const svgText = this.svgRenderer.toString(true /* shouldInjectFonts */); - this.cachedUrl = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`; - } else { - this.cachedUrl = this.props.vm.runtime.storage.get(this.props.assetId).encodeDataURI(); - } + if (this.decodedAssetId === this.props.assetId) { + // @todo consider caching more than one URL. return this.cachedUrl; } - return this.props.vm.runtime.storage.get(this.props.assetId).encodeDataURI(); + + this.decodedAssetId = this.props.assetId; + this.cachedUrl = getCostumeUrl(this.props.assetId, this.props.vm); + + return this.cachedUrl; } handleMouseUp () { this.initialOffset = null; @@ -155,7 +140,7 @@ class SpriteSelectorItem extends React.Component { } = this.props; return ( <SpriteSelectorItemComponent - costumeURL={this.getCostumeUrl()} + costumeURL={this.getCostumeData()} onClick={this.handleClick} onDeleteButtonClick={onDeleteButtonClick ? this.handleDelete : null} onDuplicateButtonClick={onDuplicateButtonClick ? this.handleDuplicate : null} @@ -209,7 +194,4 @@ const ConnectedComponent = connect( mapDispatchToProps )(SpriteSelectorItem); -export { - ConnectedComponent as default, - HAS_FONT_REGEXP // Exposed for testing -}; +export default ConnectedComponent; diff --git a/src/containers/watermark.jsx b/src/containers/watermark.jsx new file mode 100644 index 000000000..17a85dab7 --- /dev/null +++ b/src/containers/watermark.jsx @@ -0,0 +1,83 @@ +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {connect} from 'react-redux'; + +import VM from 'scratch-vm'; +import getCostumeUrl from '../lib/get-costume-url'; + +import WatermarkComponent from '../components/watermark/watermark.jsx'; + +class Watermark extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'getCostumeData' + ]); + // Asset ID of the current sprite's current costume + this.decodedAssetId = null; + } + + getCostumeData () { + if (!this.props.assetId) return null; + + if (this.decodedAssetId === this.props.assetId) { + return this.cachedUrl; + } + + this.decodedAssetId = this.props.assetId; + this.cachedUrl = getCostumeUrl(this.props.assetId, this.props.vm); + + return this.cachedUrl; + } + + render () { + const { + /* eslint-disable no-unused-vars */ + assetId, + vm, + /* eslint-enable no-unused-vars */ + ...props + } = this.props; + return ( + <WatermarkComponent + costumeURL={this.getCostumeData()} + {...props} + /> + ); + } +} + +Watermark.propTypes = { + assetId: PropTypes.string, + vm: PropTypes.instanceOf(VM).isRequired +}; + +const mapStateToProps = state => { + const targets = state.scratchGui.targets; + const currentTargetId = targets.editingTarget; + + let assetId; + if (currentTargetId) { + if (targets.stage.id === currentTargetId) { + assetId = targets.stage.costume.assetId; + } else if (targets.sprites.hasOwnProperty(currentTargetId)) { + const currentSprite = targets.sprites[currentTargetId]; + assetId = currentSprite.costume.assetId; + } + } + + return { + vm: state.scratchGui.vm, + assetId: assetId + }; +}; + +const mapDispatchToProps = () => ({}); + +const ConnectedComponent = connect( + mapStateToProps, + mapDispatchToProps +)(Watermark); + +export default ConnectedComponent; diff --git a/src/lib/get-costume-url.js b/src/lib/get-costume-url.js new file mode 100644 index 000000000..b4d779d04 --- /dev/null +++ b/src/lib/get-costume-url.js @@ -0,0 +1,28 @@ +import {SVGRenderer} from 'scratch-svg-renderer'; + +// Contains 'font-family', but doesn't only contain 'font-family="none"' +const HAS_FONT_REGEXP = 'font-family(?!="none")'; + +const getCostumeUrl = function (assetId, vm) { + + const storage = vm.runtime.storage; + const asset = storage.get(assetId); + // If the SVG refers to fonts, they must be inlined in order to display correctly in the img tag. + // Avoid parsing the SVG when possible, since it's expensive. + if (asset.assetType === storage.AssetType.ImageVector) { + // If the asset ID has not changed, no need to re-parse + + const svgRenderer = new SVGRenderer(); + + const svgString = vm.runtime.storage.get(assetId).decodeText(); + if (svgString.match(HAS_FONT_REGEXP)) { + svgRenderer.loadString(svgString); + const svgText = svgRenderer.toString(true /* shouldInjectFonts */); + return `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`; + } + return vm.runtime.storage.get(assetId).encodeDataURI(); + } + return vm.runtime.storage.get(assetId).encodeDataURI(); +}; + +export default getCostumeUrl; -- GitLab