Skip to content
Snippets Groups Projects
Unverified Commit e7ea9558 authored by Karishma Chadha's avatar Karishma Chadha Committed by GitHub
Browse files

Merge pull request #3342 from kchadha/selection-state-watermark

Add sprite/stage watermark to the blocks workspace.
parents 055b2fc5 f8251cc6
No related branches found
No related tags found
No related merge requests found
...@@ -279,6 +279,20 @@ $fade-out-distance: 15px; ...@@ -279,6 +279,20 @@ $fade-out-distance: 15px;
margin-top: 0; 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 */
.menu-bar-position { .menu-bar-position {
......
...@@ -20,6 +20,7 @@ import Box from '../box/box.jsx'; ...@@ -20,6 +20,7 @@ import Box from '../box/box.jsx';
import MenuBar from '../menu-bar/menu-bar.jsx'; import MenuBar from '../menu-bar/menu-bar.jsx';
import CostumeLibrary from '../../containers/costume-library.jsx'; import CostumeLibrary from '../../containers/costume-library.jsx';
import BackdropLibrary from '../../containers/backdrop-library.jsx'; import BackdropLibrary from '../../containers/backdrop-library.jsx';
import Watermark from '../../containers/watermark.jsx';
import Backpack from '../../containers/backpack.jsx'; import Backpack from '../../containers/backpack.jsx';
import PreviewModal from '../../containers/preview-modal.jsx'; import PreviewModal from '../../containers/preview-modal.jsx';
...@@ -271,6 +272,9 @@ const GUIComponent = props => { ...@@ -271,6 +272,9 @@ const GUIComponent = props => {
/> />
</button> </button>
</Box> </Box>
<Box className={styles.watermark}>
<Watermark />
</Box>
</TabPanel> </TabPanel>
<TabPanel className={tabClassNames.tabPanel}> <TabPanel className={tabClassNames.tabPanel}>
{costumesTabVisible ? <CostumeTab vm={vm} /> : null} {costumesTabVisible ? <CostumeTab vm={vm} /> : null}
......
.sprite-image {
margin: auto;
user-select: none;
max-width: 48px;
max-height: 48px;
opacity: 0.35;
pointer-events: none;
}
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'; ...@@ -7,19 +7,17 @@ import {setHoveredSprite} from '../reducers/hovered-target';
import {updateAssetDrag} from '../reducers/asset-drag'; import {updateAssetDrag} from '../reducers/asset-drag';
import {getEventXY} from '../lib/touch-utils'; import {getEventXY} from '../lib/touch-utils';
import VM from 'scratch-vm'; 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'; import SpriteSelectorItemComponent from '../components/sprite-selector-item/sprite-selector-item.jsx';
const dragThreshold = 3; // Same as the block drag threshold 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 { class SpriteSelectorItem extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
bindAll(this, [ bindAll(this, [
'getCostumeUrl', 'getCostumeData',
'handleClick', 'handleClick',
'handleDelete', 'handleDelete',
'handleDuplicate', 'handleDuplicate',
...@@ -30,8 +28,8 @@ class SpriteSelectorItem extends React.Component { ...@@ -30,8 +28,8 @@ class SpriteSelectorItem extends React.Component {
'handleMouseMove', 'handleMouseMove',
'handleMouseUp' 'handleMouseUp'
]); ]);
this.svgRenderer = new SVGRenderer();
// Asset ID of the SVG currently in SVGRenderer // Asset ID of the current decoded costume
this.decodedAssetId = null; this.decodedAssetId = null;
} }
shouldComponentUpdate (nextProps) { shouldComponentUpdate (nextProps) {
...@@ -44,32 +42,11 @@ class SpriteSelectorItem extends React.Component { ...@@ -44,32 +42,11 @@ class SpriteSelectorItem extends React.Component {
} }
return false; return false;
} }
getCostumeUrl () { getCostumeData () {
if (this.props.costumeURL) return this.props.costumeURL; if (this.props.costumeURL) return this.props.costumeURL;
if (!this.props.assetId) return null; if (!this.props.assetId) return null;
const storage = this.props.vm.runtime.storage; return getCostumeUrl(this.props.assetId, this.props.vm);
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();
}
return this.cachedUrl;
}
return this.props.vm.runtime.storage.get(this.props.assetId).encodeDataURI();
} }
handleMouseUp () { handleMouseUp () {
this.initialOffset = null; this.initialOffset = null;
...@@ -155,7 +132,7 @@ class SpriteSelectorItem extends React.Component { ...@@ -155,7 +132,7 @@ class SpriteSelectorItem extends React.Component {
} = this.props; } = this.props;
return ( return (
<SpriteSelectorItemComponent <SpriteSelectorItemComponent
costumeURL={this.getCostumeUrl()} costumeURL={this.getCostumeData()}
onClick={this.handleClick} onClick={this.handleClick}
onDeleteButtonClick={onDeleteButtonClick ? this.handleDelete : null} onDeleteButtonClick={onDeleteButtonClick ? this.handleDelete : null}
onDuplicateButtonClick={onDuplicateButtonClick ? this.handleDuplicate : null} onDuplicateButtonClick={onDuplicateButtonClick ? this.handleDuplicate : null}
...@@ -209,7 +186,4 @@ const ConnectedComponent = connect( ...@@ -209,7 +186,4 @@ const ConnectedComponent = connect(
mapDispatchToProps mapDispatchToProps
)(SpriteSelectorItem); )(SpriteSelectorItem);
export { export default ConnectedComponent;
ConnectedComponent as default,
HAS_FONT_REGEXP // Exposed for testing
};
import bindAll from 'lodash.bindall';
import omit from 'lodash.omit';
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;
return getCostumeUrl(this.props.assetId, this.props.vm);
}
render () {
const componentProps = omit(this.props, ['assetId', 'vm']);
return (
<WatermarkComponent
costumeURL={this.getCostumeData()}
{...componentProps}
/>
);
}
}
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 ConnectedComponent = connect(
mapStateToProps
)(Watermark);
export default ConnectedComponent;
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 () {
let cachedAssetId;
let cachedUrl;
return function (assetId, vm) {
if (cachedAssetId === assetId) {
return cachedUrl;
}
cachedAssetId = assetId;
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) {
const svgString = vm.runtime.storage.get(assetId).decodeText();
if (svgString.match(HAS_FONT_REGEXP)) {
const svgRenderer = new SVGRenderer();
svgRenderer.loadString(svgString);
const svgText = svgRenderer.toString(true /* shouldInjectFonts */);
cachedUrl = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`;
}
cachedUrl = vm.runtime.storage.get(assetId).encodeDataURI();
}
cachedUrl = vm.runtime.storage.get(assetId).encodeDataURI();
return cachedUrl;
};
}());
export {
getCostumeUrl as default,
HAS_FONT_REGEXP
};
...@@ -4,7 +4,6 @@ import configureStore from 'redux-mock-store'; ...@@ -4,7 +4,6 @@ import configureStore from 'redux-mock-store';
import {Provider} from 'react-redux'; import {Provider} from 'react-redux';
import SpriteSelectorItem from '../../../src/containers/sprite-selector-item'; import SpriteSelectorItem from '../../../src/containers/sprite-selector-item';
import {HAS_FONT_REGEXP} from '../../../src/containers/sprite-selector-item';
import CloseButton from '../../../src/components/close-button/close-button'; import CloseButton from '../../../src/components/close-button/close-button';
describe('SpriteSelectorItem Container', () => { describe('SpriteSelectorItem Container', () => {
...@@ -56,12 +55,4 @@ describe('SpriteSelectorItem Container', () => { ...@@ -56,12 +55,4 @@ describe('SpriteSelectorItem Container', () => {
wrapper.find(CloseButton).simulate('click'); wrapper.find(CloseButton).simulate('click');
expect(onDeleteButtonClick).toHaveBeenCalledWith(1337); expect(onDeleteButtonClick).toHaveBeenCalledWith(1337);
}); });
test('Has font regexp works', () => {
expect('font-family="Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy();
expect('font-family="none" font-family="Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy();
expect('font-family = "Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy();
expect('font-family="none"'.match(HAS_FONT_REGEXP)).toBeFalsy();
});
}); });
import {HAS_FONT_REGEXP} from '../../../src/lib/get-costume-url';
describe('SVG Font Parsing', () => {
test('Has font regexp works', () => {
expect('font-family="Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy();
expect('font-family="none" font-family="Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy();
expect('font-family = "Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy();
expect('font-family="none"'.match(HAS_FONT_REGEXP)).toBeFalsy();
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment