Add sprite/stage watermark to the blocks workspace.
This commit is contained in:
1 parent
a9f1e2ee3b
commit
d05b56a595
7 files changed
+169
-32
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
.sprite-image {
|
||||
margin: auto;
|
||||
user-select: none;
|
||||
max-width: 48px;
|
||||
max-height: 48px;
|
||||
opacity: 0.35;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user