Skip to content
Snippets Groups Projects
Commit 465e28a2 authored by Paul Kaplan's avatar Paul Kaplan
Browse files

Use img instead of CostumeCanvas

parent f9b58453
No related branches found
No related tags found
No related merge requests found
import PropTypes from 'prop-types';
import React from 'react';
import svgToImage from 'svg-to-image';
import xhr from 'xhr';
/**
* @fileoverview
* A component for rendering Scratch costume URLs to canvases.
* Use for sprite library, costume library, sprite selector, etc.
* Props include width, height, and direction (direction in Scratch value).
*/
class CostumeCanvas extends React.Component {
componentDidMount () {
this.load();
}
componentDidUpdate (prevProps) {
if (prevProps.url !== this.props.url) {
this.load();
} else if (
prevProps.width !== this.props.width ||
prevProps.height !== this.props.height ||
prevProps.direction !== this.props.direction
) {
this.draw();
}
}
draw () {
if (!this.canvas) {
return;
}
// Draw the costume to the rendered canvas.
const img = this.img;
const context = this.canvas.getContext('2d');
// Scale to fit.
let scale;
// Choose the larger dimension to scale by.
if (img.width > img.height) {
scale = this.canvas.width / img.width;
} else {
scale = this.canvas.height / img.height;
}
// Rotate by the Scratch-value direction.
const angle = (-90 + this.props.direction) * Math.PI / 180;
// Rotation origin point will be center of the canvas.
const contextTranslateX = this.canvas.width / 2;
const contextTranslateY = this.canvas.height / 2;
// First, clear the canvas.
context.clearRect(0, 0,
this.canvas.width, this.canvas.height);
// Translate the context to the center of the canvas,
// then rotate canvas drawing by `angle`.
context.translate(contextTranslateX, contextTranslateY);
context.rotate(angle);
context.drawImage(img,
0, 0, img.width, img.height,
-(scale * img.width / 2), -(scale * img.height / 2),
scale * img.width,
scale * img.height);
// Reset the canvas rotation and translation to 0, (0, 0).
context.rotate(-angle);
context.translate(-contextTranslateX, -contextTranslateY);
}
load () {
// Draw the icon on our canvas.
const url = this.props.url;
if (url.indexOf('.svg') > -1) {
// Vector graphics: need to download with XDR and rasterize.
// Queue request asynchronously.
setTimeout(() => {
xhr.get({
useXDR: true,
url: url
}, (err, response, body) => {
if (!err) {
svgToImage(body, (svgErr, img) => {
if (!svgErr) {
this.img = img;
this.draw();
}
});
}
});
}, 0);
} else {
// Raster graphics: create Image and draw it.
const img = new Image();
img.src = url;
img.onload = () => {
this.img = img;
this.draw();
};
}
}
render () {
return (
<canvas
className={this.props.className}
height={this.props.height * (window.devicePixelRatio || 1)}
style={{
height: `${this.props.height}px`,
width: `${this.props.width}px`
}}
width={this.props.width * (window.devicePixelRatio || 1)}
ref={c => (this.canvas = c)} // eslint-disable-line react/jsx-sort-props
/>
);
}
}
CostumeCanvas.defaultProps = {
width: 100,
height: 100,
direction: 90
};
CostumeCanvas.propTypes = {
className: PropTypes.string,
direction: PropTypes.number,
height: PropTypes.number,
url: PropTypes.string.isRequired,
width: PropTypes.number
};
export default CostumeCanvas;
......@@ -42,6 +42,8 @@
.sprite-image {
margin: auto;
user-select: none;
max-width: 32px;
max-height: 32px;
}
.sprite-info {
......
......@@ -2,7 +2,6 @@ import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import CostumeCanvas from '../costume-canvas/costume-canvas.jsx';
import CloseButton from '../close-button/close-button.jsx';
import styles from './sprite-selector-item.css';
import {ContextMenuTrigger} from 'react-contextmenu';
......@@ -38,11 +37,9 @@ const SpriteSelectorItem = props => (
<div className={styles.number}>{props.number}</div>
)}
{props.costumeURL ? (
<CostumeCanvas
<img
className={styles.spriteImage}
height={32}
url={props.costumeURL}
width={32}
src={props.costumeURL}
/>
) : null}
<div className={styles.spriteInfo}>
......
......@@ -89,6 +89,8 @@ $header-height: calc($stage-menu-height - 2px);
border: 1px solid $ui-black-transparent;
border-radius: .25rem;
box-shadow: inset 0 0 4px $ui-black-transparent;
max-width: 64px;
max-height: 48px;
}
.add-button {
......
......@@ -5,7 +5,6 @@ import {defineMessages, intlShape, injectIntl, FormattedMessage} from 'react-int
import Box from '../box/box.jsx';
import ActionMenu from '../action-menu/action-menu.jsx';
import CostumeCanvas from '../costume-canvas/costume-canvas.jsx';
import styles from './stage-selector.css';
import backdropIcon from '../action-menu/icon--backdrop.svg';
......@@ -78,11 +77,9 @@ const StageSelector = props => {
</div>
</div>
{url ? (
<CostumeCanvas
<img
className={styles.costumeCanvas}
height={48}
url={url}
width={64}
src={url}
/>
) : null}
<div className={styles.label}>
......
......@@ -30,16 +30,9 @@ exports[`SpriteSelectorItemComponent matches snapshot when given a number and de
>
5
</div>
<canvas
<img
className={undefined}
height={32}
style={
Object {
"height": "32px",
"width": "32px",
}
}
width={32}
src="https://scratch.mit.edu/foo/bar/pony"
/>
<div
className={undefined}
......@@ -113,16 +106,9 @@ exports[`SpriteSelectorItemComponent matches snapshot when selected 1`] = `
src="test-file-stub"
/>
</div>
<canvas
<img
className={undefined}
height={32}
style={
Object {
"height": "32px",
"width": "32px",
}
}
width={32}
src="https://scratch.mit.edu/foo/bar/pony"
/>
<div
className={undefined}
......
import React from 'react';
import {mountWithIntl, shallowWithIntl, componentWithIntl} from '../../helpers/intl-helpers.jsx';
import SpriteSelectorItemComponent from '../../../src/components/sprite-selector-item/sprite-selector-item';
import CostumeCanvas from '../../../src/components/costume-canvas/costume-canvas';
import CloseButton from '../../../src/components/close-button/close-button';
describe('SpriteSelectorItemComponent', () => {
......@@ -73,17 +72,6 @@ describe('SpriteSelectorItemComponent', () => {
expect(onDeleteButtonClick).toHaveBeenCalled();
});
test('creates a CostumeCanvas when a costume url is defined', () => {
const wrapper = shallowWithIntl(getComponent());
expect(wrapper.find(CostumeCanvas).exists()).toBe(true);
});
test('does not create a CostumeCanvas when a costume url is null', () => {
costumeURL = null;
const wrapper = shallowWithIntl(getComponent());
expect(wrapper.find(CostumeCanvas).exists()).toBe(false);
});
test('it has a context menu with delete menu item and callback', () => {
const wrapper = mountWithIntl(getComponent());
const contextMenu = wrapper.find('ContextMenu');
......
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