Skip to content

Commit

Permalink
Merge pull request #246 from google/try/masks
Browse files Browse the repository at this point in the history
Prototype of clipping masks
  • Loading branch information
Dima Voytenko authored Feb 7, 2020
2 parents a9b7eca + b0b297c commit 8089e14
Show file tree
Hide file tree
Showing 12 changed files with 295 additions and 14 deletions.
5 changes: 4 additions & 1 deletion assets/src/edit-story/components/canvas/displayElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
import StoryPropTypes from '../../types';
import { useTransformHandler } from '../transform';
import { useUnits } from '../../units';
import { WithElementMask } from '../../masks';

const Wrapper = styled.div`
${elementWithPosition}
Expand Down Expand Up @@ -72,7 +73,9 @@ function DisplayElement({ element }) {

return (
<Wrapper ref={wrapperRef} {...box}>
<Display element={element} box={box} />
<WithElementMask element={element} fill={true}>
<Display element={element} box={box} />
</WithElementMask>
</Wrapper>
);
}
Expand Down
9 changes: 8 additions & 1 deletion assets/src/edit-story/components/canvas/frameElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@ import {
elementWithRotation,
} from '../../elements/shared';
import { useUnits } from '../../units';
import { WithElementMask } from '../../masks';
import useCanvas from './useCanvas';

// @todo: should the frame borders follow clip lines?

// Pointer events are disabled in the display mode to ensure that selection
// can be limited to the mask.
const Wrapper = styled.div`
${elementWithPosition}
${elementWithSize}
Expand Down Expand Up @@ -85,7 +90,9 @@ function FrameElement({ element }) {
evt.stopPropagation();
}}
>
{Frame && <Frame element={element} box={box} />}
<WithElementMask element={element} fill={true}>
{Frame && <Frame element={element} box={box} />}
</WithElementMask>
</Wrapper>
);
}
Expand Down
5 changes: 5 additions & 0 deletions assets/src/edit-story/components/panels/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import ColorPanel from './color';
import BackgroundColorPanel from './backgroundColor';
import FillPanel from './fill';
import FontPanel from './font';
import MaskPanel from './mask';
import RotationPanel from './rotationAngle';
import SizePanel from './size';
import PositionPanel from './position';
Expand All @@ -47,6 +48,7 @@ const FILL = 'fill';
const BACKGROUND_COLOR = 'backgroundColor';
const STYLE = 'style';
const VIDEO_POSTER = 'videoPoster';
const MASK = 'mask';

export const PanelTypes = {
ACTIONS,
Expand All @@ -62,6 +64,7 @@ export const PanelTypes = {
ROTATION_ANGLE,
FILL,
VIDEO_POSTER,
MASK,
};

const ALL = Object.values(PanelTypes);
Expand Down Expand Up @@ -126,6 +129,8 @@ export function getPanels(elements) {
return { type, Panel: TextPanel };
case VIDEO_POSTER:
return { type, Panel: VideoPosterPanel };
case MASK:
return { type, Panel: MaskPanel };
default:
throw new Error(`Unknown panel: ${type}`);
}
Expand Down
91 changes: 91 additions & 0 deletions assets/src/edit-story/components/panels/mask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* External dependencies
*/
import PropTypes from 'prop-types';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { MASKS } from '../../masks';
import { SimplePanel } from './panel';
import getCommonValue from './utils/getCommonValue';

/* eslint-disable jsx-a11y/no-onchange */
function MaskPanel({ selectedElements, onSetProperties }) {
const masks = selectedElements.map(({ mask }) => mask);
const type = masks.some((mask) => !mask) ? '' : getCommonValue(masks, 'type');
const mask = MASKS.find((aMask) => aMask.type === type);

const onTypeChanged = (evt) => {
const newType = evt.target.value;
if (newType === '') {
onSetProperties({ mask: null });
} else {
const newMask = MASKS.find((aMask) => aMask.type === newType);
onSetProperties({
mask: {
type: newType,
...newMask.defaultProps,
},
});
}
};

return (
<SimplePanel name="mask" title={__('Mask', 'web-stories')}>
<select value={type} onChange={onTypeChanged}>
<option key={'none'} value={''}>
{'None'}
</option>
{MASKS.map(({ type: aType, name }) => (
<option key={aType} value={aType}>
{name}
</option>
))}
</select>
<div>
{mask && (
<svg width={50} height={50} viewBox="0 0 1 1">
<g transform="scale(0.8)" transform-origin="50% 50%">
<path
d={mask.path}
fill="none"
stroke="blue"
strokeWidth={2 / 50}
/>
</g>
</svg>
)}
</div>
</SimplePanel>
);
}
/* eslint-enable jsx-a11y/no-onchange */

MaskPanel.propTypes = {
selectedElements: PropTypes.array.isRequired,
onSetProperties: PropTypes.func.isRequired,
};

export default MaskPanel;
32 changes: 22 additions & 10 deletions assets/src/edit-story/elements/image/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
} from '../shared';
import { useStory } from '../../app';
import StoryPropTypes from '../../types';
import { WithElementMask } from '../../masks';
import { imageWithScale } from './util';
import EditCropMovable from './editCropMovable';

Expand All @@ -55,10 +56,19 @@ const CropImg = styled.img`
${imageWithScale}
`;

function ImageEdit({
element: { id, src, origRatio, scale, focalX, focalY, isFill, isBackground },
box: { x, y, width, height, rotationAngle },
}) {
function ImageEdit({ element, box }) {
const {
id,
src,
origRatio,
scale,
focalX,
focalY,
isFill,
isBackground,
} = element;
const { x, y, width, height, rotationAngle } = box;

const [fullImage, setFullImage] = useState(null);
const [croppedImage, setCroppedImage] = useState(null);
const [cropBox, setCropBox] = useState(null);
Expand All @@ -84,12 +94,14 @@ function ImageEdit({
<Element>
<FadedImg ref={setFullImage} draggable={false} src={src} {...imgProps} />
<CropBox ref={setCropBox}>
<CropImg
ref={setCroppedImage}
draggable={false}
src={src}
{...imgProps}
/>
<WithElementMask element={element} fill={true}>
<CropImg
ref={setCroppedImage}
draggable={false}
src={src}
{...imgProps}
/>
</WithElementMask>
</CropBox>

{!isFill && !isBackground && cropBox && croppedImage && (
Expand Down
1 change: 1 addition & 0 deletions assets/src/edit-story/elements/image/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ export const panels = [
PanelTypes.SCALE,
PanelTypes.ROTATION_ANGLE,
PanelTypes.FILL,
PanelTypes.MASK,
];
1 change: 1 addition & 0 deletions assets/src/edit-story/elements/square/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ export const panels = [
PanelTypes.BACKGROUND_COLOR,
PanelTypes.ROTATION_ANGLE,
PanelTypes.FILL,
PanelTypes.MASK,
];
1 change: 1 addition & 0 deletions assets/src/edit-story/elements/video/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ export const panels = [
PanelTypes.ROTATION_ANGLE,
PanelTypes.VIDEO_POSTER,
PanelTypes.FILL,
PanelTypes.MASK,
];
5 changes: 5 additions & 0 deletions assets/src/edit-story/masks/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"no-restricted-imports": ["error", { "paths": ["styled-components", "@wordpress/element"] }]
}
}
148 changes: 148 additions & 0 deletions assets/src/edit-story/masks/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* External dependencies
*/
import PropTypes from 'prop-types';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import StoryPropTypes from '../types';

// Important! This file cannot use `styled-components` or any stateful/context
// React features to stay compatible with the "output" templates.

const MaskIds = {
HEART: 'heart',
STAR: 'star',
};

const CLIP_PATHS = {
// @todo: This is a very bad heart.
[MaskIds.HEART]:
'M 0.5,1 C 0.5,1,0,0.7,0,0.3 A 0.25,0.25,1,1,1,0.5,0.3 A 0.25,0.25,1,1,1,1,0.3 C 1,0.7,0.5,1,0.5,1 Z',
// @todo: This is a horrible star.
[MaskIds.STAR]: 'M .5,0 L .8,1 L 0,.4 L 1,.4 L .2,1 Z',
// viewbox = [0 0 163 155]
// M 81.5 0 L 100.696 59.079 H 162.815 L 112.56 95.5919 L 131.756 154.671 L 81.5 118.158 L 31.2444 154.671 L 50.4403 95.5919 L 0.184669 59.079H62.3041L81.5 0 Z
};

export const MASKS = [
{
type: MaskIds.HEART,
name: __('Heart', 'web-stories'),
path: CLIP_PATHS[MaskIds.HEART],
},
{
type: MaskIds.STAR,
name: __('Star', 'web-stories'),
path: CLIP_PATHS[MaskIds.STAR],
},
];

const FILL_STYLE = {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
};

export function WithElementMask({ element, fill, style, children, ...rest }) {
const mask = getElementMaskProperties(element);
return (
<WithtMask
fill={fill}
style={style}
mask={mask}
elementId={element.id}
{...rest}
>
{children}
</WithtMask>
);
}

WithElementMask.propTypes = {
element: StoryPropTypes.element.isRequired,
style: PropTypes.object,
fill: PropTypes.bool,
children: StoryPropTypes.children.isRequired,
};

function WithtMask({ elementId, mask, fill, style, children, ...rest }) {
const maskType = (mask && mask.type) || null;

const fillStyle = fill ? FILL_STYLE : null;

const allStyles = {
...fillStyle,
...style,
};

if (maskType) {
// @todo: Chrome cannot do inline clip-path using data: URLs.
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1041024.

const maskId = `mask-${maskType}-${elementId}`;
allStyles.clipPath = `url(#${maskId})`;

return (
<div style={allStyles} {...rest}>
<svg width={0} height={0}>
<defs>
<clipPath id={maskId} clipPathUnits="objectBoundingBox">
<path d={CLIP_PATHS[maskType]} />
</clipPath>
</defs>
</svg>
{children}
</div>
);
}
return (
<div style={allStyles} {...rest}>
{children}
</div>
);
}

WithtMask.propTypes = {
elementId: PropTypes.string.isRequired,
mask: StoryPropTypes.mask,
style: PropTypes.object,
fill: PropTypes.bool,
children: StoryPropTypes.children.isRequired,
};

function getElementMaskProperties({ type, mask, ...rest }) {
if (mask) {
return mask;
}
return getDefaultElementMaskProperties({ type, ...rest });
}

function getDefaultElementMaskProperties({}) {
// @todo: mask-based shapes (square, circle, etc) automatically assume masks.
return null;
}
Loading

0 comments on commit 8089e14

Please sign in to comment.