
提到“clip-path工具”我们之前已经分享过了,不知道大家有没有印象,如果您还没阅读过,请链接下面链接:
轻松绘制几何图形的裁剪路径(clip-path)工具:Clippy
当然,我们今天不是分享这个工具,而是通过它去延伸拓展。首先我们来看下效果图:

我们通过有四个选项按钮:
Circle
Ellipse
Inset
Polygon
通过勾选不同,可以对图形进行裁剪。如果您对裁剪点不满意,可以通过“ADD NODE”进行调节。代码如下:
HTML
<div id="app"></div>
CSS
* box-sizing border-box $size = 300px $border = 4px html body background radial-gradient(circle at 50% 190px, rebeccapurple, #111 ) font-family 'Arial', sans-serif font-size 14px overflow-x hidden min-height 100vh button input label cursor pointer button label font-size 1rem button border 0 border-radius 6px color white cursor pointer min-width 120px padding 12px 12px &[disabled] &[disabled]:hover background grey cursor not-allowed opacity .25 #app padding 40px 0 align-items center display flex flex-direction column .clip-path-generator height $size width $size &__container border $border solid #fff height $size + (2 * $border) position relative width $size + (2 * $border) &__clipped background url(https://placebear.com/300/300) #2eec71 background-size cover height 100% left 0 position absolute top 0 width 100% clip-path var(--path) .clip-path-node cursor move cursor -webkit-grab height 50px left calc(var(--x) * 1px) opacity 0.75 position absolute top calc(var(--y) * 1px) transform translate(-50%, -50%) transition opacity .15s width 50px z-index 1 &:hover opacity 1 &:after background #fff border-radius 100% content '' height 20px left 50% position absolute top 50% transform translate(-50%, -50%) width 20px &--removing cursor pointer opacity 1 &:after content none &__remove background #e74c3c border 2px solid white border-radius 100% height 20px left 50% position absolute top 50% transform translate(-50%, -50%) width 20px path fill #fff &--moving cursor none &:after background transparent .clip-path-options display flex flex-wrap wrap margin-bottom 30px width 200px .clip-path-option align-items center display flex height 30px width 50% &__input height 0 opacity 0 width 0 &:checked ~ label color dodgerblue opacity 1 .clip-path-option__check border-color dodgerblue &:after background dodgerblue &[disabled] ~ label cursor not-allowed opacity .25 &:hover .clip-path-option__check:after background none &:checked ~ label:hover .clip-path-option__check:after background dodgerblue &__label align-items center color #fff display flex opacity .35 &:hover opacity 1 .clip-path-option__check:after background white &__check border 4px solid white border-radius 100% height 24px margin-right 10px position relative width 24px &:after background transparent border-radius 100% content '' height 75% left 50% position absolute top 50% transform translate(-50%, -50%) width 75% .clip-path color #ffffff margin 10px 0 30px 0 max-width 500px text-align center &__copy background dodgerblue font-size 1.5rem width 200px &:hover background darken(dodgerblue, 15%) &:active background darken(dodgerblue, 25%) &__value background #111 border-radius 6px margin-bottom 10px padding 20px &__input left 100% position fixed .polygon-actions display flex width 300px flex-wrap wrap align-items center justify-content center .polygon-action background #ecf0f1 color #111 margin 4px &:hover background darken(#ecf0f1, 15%) &:active background darken(#ecf0f1, 25%) &--removing background #e74c3c color #fff &:hover background darken(#e74c3c, 15%) &:active background darken(#e74c3c, 25%)
JS
const { Component, Fragment } = React
const { render } = ReactDOM
const getClipPath = (nodes, size, mode) => {
let path = ''
let centerX
let centerY
let dragX
let dragY
const getRatio = d => Math.floor(d / size * 100)
if (!nodes.length) return null
switch (mode) {
case MODES.POLYGON:
for (let n = 0; n < nodes.length; n++) {
const { x, y } = nodes[n]
path += `${getRatio(x)}% ${getRatio(y)}%${
n === nodes.length - 1 ? '' : `, `
}`
}
break
case MODES.INSET:
for (let n = 0; n < nodes.length; n++) {
const { x, y } = nodes[n]
switch (n) {
case 0:
path += `${getRatio(y)}% `
break
case 1:
path += `${100 - getRatio(x)}% `
break
case 2:
path += `${100 - getRatio(y)}% `
break
case 3:
path += `${getRatio(x)}%`
break
}
}
break
case MODES.ELLIPSE:
;({ x: centerX, y: centerY } = nodes[0])
dragX = nodes[1].x
dragY = nodes[2].y
const xDistance = Math.abs(getRatio(dragX - centerX))
const yDistance = Math.abs(getRatio(dragY - centerY))
path = `${xDistance}% ${yDistance}% at ${getRatio(centerX)}% ${getRatio(
centerY
)}%`
break
case MODES.CIRCLE:
;({ x: centerX, y: centerY } = nodes[0])
dragX = nodes[1].x
dragY = nodes[1].y
const distX = dragX - centerX
const distY = dragY - centerY
const distance = getRatio(Math.sqrt(distX * distX + distY * distY))
path = `${distance}% at ${getRatio(centerX)}% ${getRatio(centerY)}%`
break
default:
path = 'No mode selected'
}
return `${mode.toLowerCase()}(${path})`
}
class ClipPathNode extends Component {
state = {
moving: false,
x: this.props.x,
y: this.props.y,
}
componentDidMount = () => {
const { el, startMove, removeNode } = this
el.addEventListener('mousedown', startMove)
el.addEventListener('touchstart', startMove)
el.addEventListener('click', removeNode)
}
removeNode = () => {
const { id, removing, onRemove } = this.props
if (removing && onRemove) onRemove(id)
}
getPageXY = e => {
let { pageX: x, pageY: y, touches } = e
if (touches && touches.length === 1) {
x = touches[0].pageX
y = touches[0].pageY
}
y -= window.pageYOffset
return {
x,
y,
}
}
startMove = e => {
const { onMove, removing, restrictX, restrictY } = this.props
if (removing) return
const { x, y } = this.getPageXY(e)
const move = e => {
const { x: oldX, y: oldY } = this.state
const { x: pageX, y: pageY } = this.getPageXY(e)
const {
height,
top,
left,
width,
} = this.el.parentNode.getBoundingClientRect()
const x = Math.min(Math.max(0, pageX - left), width)
const y = Math.min(Math.max(0, pageY - top), height)
this.setState(
{
x: restrictX ? oldX : x,
y: restrictY ? oldY : y,
},
() => onMove(this)
)
}
const endMove = () => {
this.setState(
{
moving: false,
},
() => {
onMove(this)
document.body.removeEventListener('mousemove', move)
document.body.removeEventListener('mouseup', endMove)
if (e.touches && e.touches.length === 1) {
document.body.removeEventListener('touchmove', move)
document.body.removeEventListener('touchend', endMove)
}
}
)
}
const initMove = () => {
document.body.addEventListener('mousemove', move)
document.body.addEventListener('mouseup', endMove)
if (e.touches && e.touches.length === 1) {
document.body.addEventListener('touchmove', move)
document.body.addEventListener('touchend', endMove)
}
}
this.setState(
{
moving: true,
},
initMove
)
}
componentWillReceiveProps = nextProps => {
if (nextProps.x !== this.props.x || nextProps.y !== this.props.y) {
this.setState({ x: nextProps.x, y: nextProps.y })
}
}
render = () => {
const { moving, x, y } = this.state
const { removing } = this.props
return (
<div
className={`clip-path-node ${moving ? 'clip-path-node--moving' : ''} ${
removing ? 'clip-path-node--removing' : ''
}`}
ref={n => (this.el = n)}
style={{
'--x': x,
'--y': y,
}}>
{removing && (
<svg className="clip-path-node__remove" viewBox="0 0 24 24">
<path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
</svg>
)}
</div>
)
}
}
const SIZE = 300
const PRESETS = {
CIRCLE: [
{
x: 195,
y: 105,
},
{
x: 140,
y: 50,
},
],
ELLIPSE: [
{
x: 195,
y: 150,
},
{
x: 285,
y: 150,
},
{
x: 195,
y: 30,
},
],
INSET: [
{
x: 195,
y: 30,
},
{
x: 270,
y: 165,
},
{
x: 195,
y: 300,
},
{
x: 120,
y: 165,
},
],
POLYGON: [
{
x: 255,
y: 20,
},
{
x: 210,
y: 45,
},
{
x: 90,
y: 45,
},
{
x: 45,
y: 15,
},
{
x: 0,
y: 75,
},
{
x: 30,
y: 135,
},
{
x: 30,
y: 225,
},
{
x: 75,
y: 270,
},
{
x: 120,
y: 285,
},
{
x: 180,
y: 285,
},
{
x: 225,
y: 275,
},
{
x: 270,
y: 225,
},
{
x: 270,
y: 135,
},
{
x: 300,
y: 75,
},
],
}
const MODES = {
CIRCLE: 'CIRCLE',
ELLIPSE: 'ELLIPSE',
INSET: 'INSET',
POLYGON: 'POLYGON',
}
class ClipPathGenerator extends Component {
state = {
removing: false,
mode: MODES.POLYGON,
size: SIZE,
nodes: PRESETS[MODES.POLYGON],
}
onUpdate = n => {
const {id: updatedId} = n.props
const {x: updatedX, y: updatedY} = n.state
const { mode, nodes: currentNodes } = this.state
const getHandlePoint = (node, axis) => {
const movement = n.state[axis] - currentNodes[0][axis]
let newPoint = node[axis] + movement
if (newPoint > SIZE || newPoint < 0) {
newPoint = currentNodes[0][axis] + currentNodes[0][axis] - newPoint
}
return Math.min(SIZE, Math.max(0, newPoint))
}
// Create a new set of nodes by mapping through the current node set
const nodes = currentNodes.map((o, idx) => {
// If it's the node that's being moved, just update it with new values
if (idx === updatedId) {
o = Object.assign({}, o, {
x: updatedX,
y: updatedY,
})
}
// Else if in INSET mode update the corresponding axis so the nodes are center aligned
else if (mode === MODES.INSET) {
// Need to update the opposite axis to still be centrally aligned in the clip
const getCenterPoint = (start, end, axis) =>
currentNodes[start][axis] +
(currentNodes[end][axis] - currentNodes[start][axis]) / 2
// UPDATING X
if (updatedId % 2 && !(idx % 2)) {
o = Object.assign({}, o, {
x:
updatedId === 1
? getCenterPoint(3, 1, 'x')
: getCenterPoint(1, 3, 'x'),
})
} else if (!(updatedId % 2) && idx % 2) {
o = Object.assign({}, o, {
y:
updatedId === 2
? getCenterPoint(0, 2, 'y')
: getCenterPoint(2, 0, 'y'),
})
}
} else if (mode === MODES.ELLIPSE && updatedId === 0 && idx !== 0) {
o = Object.assign({}, o, {
y: idx === 1 ? updatedY : getHandlePoint(o, 'y'),
x: idx === 1 ? getHandlePoint(o, 'x') : updatedX,
})
} else if (mode === MODES.CIRCLE && updatedId === 0 && idx !== 0) {
o = Object.assign({}, o, {
x: getHandlePoint(o, 'x'),
y: getHandlePoint(o, 'y'),
})
}
return o
})
this.setState({
copied: false,
nodes,
})
}
addNode = () => {
this.setState({
copied: false,
nodes: [
...this.state.nodes,
{
x: SIZE / 2,
y: SIZE / 2,
},
],
})
}
removeNode = id => {
const nodes = this.state.nodes.filter((n, i) => i !== id)
this.setState({
nodes,
})
}
removeNodes = () => {
this.setState({
copied: false,
removing: !this.state.removing,
})
}
switchMode = e => {
this.setState({
copied: false,
mode: MODES[e.target.value.toUpperCase()],
nodes: PRESETS[e.target.value.toUpperCase()],
})
}
copy = () => {
if (this.path) {
this.path.select()
document.execCommand('Copy')
this.setState({
copied: true,
})
}
}
wipeNodes = () => {
this.setState({
nodes: []
})
}
render = () => {
const {
addNode,
copy,
switchMode,
onUpdate,
removeNode,
removeNodes,
wipeNodes,
} = this
const { copied, mode, nodes, removing, size } = this.state
const clipPath = getClipPath(nodes, size, mode)
return (
<Fragment>
<div className="clip-path-generator__container">
<div className="clip-path-generator" ref={c => (this.el = c)}>
<div
className="clip-path-generator__clipped"
style={{
'--path': clipPath,
}}
/>
{nodes.map((n, idx) => (
<ClipPathNode
id={idx}
key={`clip-node--${idx}`}
x={n.x}
y={n.y}
removing={removing}
onMove={onUpdate}
onRemove={removeNode}
restrictX={
(mode === MODES.INSET && !(idx % 2)) ||
(mode === MODES.ELLIPSE && idx === 2)
? true
: false
}
restrictY={
(mode === MODES.INSET && idx % 2) ||
(mode === MODES.ELLIPSE && idx === 1)
? true
: false
}
/>
))}
</div>
</div>
<div className="clip-path">
<div className="clip-path__value">{clipPath ? `clip-path: ${clipPath};` : 'No clip path set'}</div>
<input
className="clip-path__input"
readOnly
ref={p => (this.path = p)}
value={`clip-path: ${clipPath};`}
/>
<button
disabled={removing || !clipPath}
className="clip-path__copy button"
onClick={copy}>
{copied ? 'Copied ' : 'Copy'}
</button>
</div>
<div className="clip-path-options">
{Object.keys(MODES).map((m, idx) => (
<div key={`clip-path-option--${idx}`} className="clip-path-option">
<input
disabled={removing}
className="clip-path-option__input"
type="radio"
name="shape"
value={m}
id={`clip-path-option--${m}`}
onChange={switchMode}
checked={m === mode}
/>
<label
className="clip-path-option__label"
htmlFor={`clip-path-option--${m}`}>
<div className="clip-path-option__check" />
{`${m.charAt(0)}${m.toLowerCase().substr(1)}`}
</label>
</div>
))}
</div>
{mode === MODES.POLYGON && (
<div className="polygon-actions">
<button
className="polygon-action polygon-action--add button"
disabled={removing}
onClick={addNode}>
Add Node
</button>
<button
className={`polygon-action ${
removing ? 'polygon-action--removing' : 'polygon-action--remove'
} button`}
onClick={removeNodes}>
{removing ? 'Done' : 'Remove Node'}
</button>
<button
disabled={removing}
className='polygon-action polygon-action--wipe button'
onClick={wipeNodes}>
{'Wipe Nodes'}
</button>
</div>
)}
</Fragment>
)
}
}
render(<ClipPathGenerator />, document.getElementById('app'))预览地址:点击我
大家试试这个工具吧。








网友评论文明上网理性发言 已有0人参与
发表评论: