diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index a10b08b60a4d34e30ae137cfa4f274ab559fa6ff..6118b799753ce6d2bd3ef6033c596a37e244a8c8 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 d39e2283320384f35a81e0185c6d7d01f59fbe92..b37dcbd7902e346e63761e6f41c03f085e429410 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 0000000000000000000000000000000000000000..f02e3242c9be4adabb3d421754c56c1692cff2ea --- /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 0000000000000000000000000000000000000000..d3f5b36e24aaaeca8100b71adf833871525964be --- /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 9b154c59b0ae97a85522865db8cec792fd1ad65b..c33aa41e7b0a610a017a862741ce908ed1952bc3 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 0000000000000000000000000000000000000000..17a85dab7a7f0c7c55c57813f16b3c39387adbbd --- /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 0000000000000000000000000000000000000000..b4d779d0483c3da9cb563e3f88efa2b0d5953630 --- /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;