提到“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人参与
发表评论: