Open yuxino opened 6 years ago
程序员的三大浪漫,编译原理,图形学,操作系统。最近在弄图形学相关的东西,emmmm , 说不上图形学。。。其实也就是在弄canvas。吃饭的时候看到支付宝付款后有一个刮刮卡。于是就在想能不能做个类似的东西呢。
答案是当然可以啊。于是就弄了一个demo。弄完之后又在想可不可以弄一个react的刮刮卡组件呢。当然也可以。这篇文章将记录我怎么弄一个这样的组件。
弄组件之前。我们先看看刮刮卡效果的实现原理。demo的代码很短加上空格也不到60行。
先看第一段。创建一个Image标签。并且获取canvas元素。为之后要用到这两个东西作准备。
const img = new Image(); img.src = "http://7xqvgr.com1.z0.glb.clouddn.com/70ed7f087bf40ad17078e01b5d2c11dfabeccec4.gif"; const canvas = document.querySelector('canvas');
接下来就是监听图片的load事件。当load完成后开始做某些事情。
img.addEventListener('load', function(e) { // ... });
除此以外就没有其他的代码了。后面的篇幅讲的都是load完之后做的事情。所以会省略掉上面那段代码。只贴里面的核心逻辑。
完成刮刮卡效果的最主要的核心是通过compositing operation来控制组合图形。详细的资料可以在这里查到。还不错的中文博客。
在刮刮卡里面用到了compositing operation的destination-out属性。
destination-out
The existing content is kept where it doesn't overlap the new shape.
这是MDN对destination-out的描述。但是这段话翻译过来还是蛮抽象,蛮难理解的的。这里我用我们肉眼可见的效果。可观察的结果来说就是使用了destination-out的图形。每当新的图形如果叠加在这个图形上的话。会让这个图形和新的图形叠加的部分变透明。
这里我特意写了个demo展示了一下这个效果。如果你不理解我上面的文字描述可以看看。
如果知道了destination-out给我们带来的效果。那么实现一个擦除效果就很简单了。我们只需要简单的在一个图层上叠图形就好了。叠的图形的首选当然是圆形。因为圆形比较像人的手指。圆的大小可以自己决定。我们唯一要运算的东西只有一个,那就是x,y坐标。这两个坐标就是鼠标在画布里面的坐标。
一般来说刮刮卡 当然是要有一种刮开的感觉。当手指(鼠标)按住然后滑动才开始刮。松开后就停止刮。要实现这个逻辑的话也是非常简单。我们只需要维护一个state就好了。在开始时(mousedown)的时候把state设置为true。在松开时(mouseup)的时候把state设置为false。移(划)动(mouseup)的时候根据state判断是否开始擦除。
总结一下。我们要做的事情就是实现擦除效果。处理手机和PC的事件。codepen的例子只处理了PC。所以在react-scrath中会把他补全。
我使用了nwb构建项目。我写过一篇怎么构建项目的文章。可以看这里了解一下。但是这一篇不会告诉你怎么使用nwb。
为了写一个可以给其他人使用的sratch组件。那么我们需要定义一些可以定制化的东西。
我定了一下react-sratch需要接收的一些prop和它们的默认值。其中width,height,ratio的大小都是px。
import React, {Component} from 'react' import t from 'prop-types' export default class ReactScratch extends Component { // .... } ReactScratch.propTypes = { width: t.oneOfType([t.string,t.number]), height: t.oneOfType([t.string,t.number]), baseImg: t.string, baseBg: t.string, coverBg: t.string, ratio: t.oneOfType([t.string,t.number]), ratioSize: t.number, callback: t.func } ReactScratch.defaultProps = { width: 300, height: 200, baseImg: null, baseBg: null, coverBg: '#000', ratio: .8, ratioSize: 30, callback: () => {} }
首先我们设置一下canvas的大小并且把底图加到canvas上面吧。这里为了获得canvas的ref。用到了新的api-createRef()。可以了解一下。
const setSize = (node,props) => { const { width, height } = props node.width = width node.height = height } const getBg = (props) => { const isLink = /^https?:\/\//i const { baseBg } = props const ret = isLink.test(baseBg) ? `url(${baseBg})` : baseBg return ret } export default class ReactScratch extends Component { constructor (props) { super(props) this.canvasRef = React.createRef() } componentDidMount () { const node = this.canvasRef.current setSize(node,this.props) } render () { return ( <canvas style={{ border: '1px solid black', background: getBg(this.props), backgroundSize: 'cover' }} ref={this.canvasRef} /> ) } }
我这里放弃思考图片要怎么样。一律都是cover。我也不管图片是什么结尾。反正是https或者http开头的我都当成是链接。
cover
现在可以这样使用。传一个地址或者一个颜色(可以是rgb,hsl)。
const baseBg = "http://os33nc36m.bkt.clouddn.com/FiXqeVa9OaZHb7empMXZrETKte9F" // .... <ReactScratch baseBg={baseBg} />
现在看起来是这样子。这里用的图是博士和坂本。
好啊。设置完了这个。那就加一层遮罩(覆盖在上面的东东)吧。也就是coverBg。因为增加了一些api。整个index.js看起来有一些臃肿。所以我把设置canvas的东西抽出来放在了utils.js里面。重用了一下isLink函数。
index.js
utils.js
isLink
import React, {Component} from 'react' import t from 'prop-types' import { getBg, getContext2d, setSize, setCover } from './utils' export default class ReactScratch extends Component { constructor (props) { super(props) this.canvasRef = React.createRef() } componentDidMount () { const node = this.canvasRef.current setSize(node,this.props) setCover(node,this.props) } render () { return ( <canvas style={{ border: '1px solid black', background: getBg(this.props), backgroundSize: 'cover' }} ref={this.canvasRef} /> ) } } ReactScratch.propTypes = { width: t.oneOfType([t.string,t.number]), height: t.oneOfType([t.string,t.number]), baseImg: t.string, baseBg: t.string, coverBg: t.string, ratio: t.oneOfType([t.string,t.number]), ratioSize: t.number, callback: t.func } ReactScratch.defaultProps = { width: 300, height: 200, baseImg: null, baseBg: null, coverBg: '#000', ratio: .8, ratioSize: 30, callback: () => {} }
/** * @private check string is a link ? * @param {*} str */ const isLink = str => /^https?:\/\//i.test(str) /** * @public get canvas context2d * @param {*} ref */ const getContext2d = (ref) => { const ctx = ref.getContext('2d') return ctx } /** * @public get base background of canvas * @param {*} props */ const getBg = (props) => { const { baseBg } = props const ret = isLink(baseBg) ? `url(${baseBg})` : baseBg return ret } /** * @public set canvas size * @param {*} node * @param {*} props */ const setSize = (node, props) => { const { width, height } = props node.width = width node.height = height } /** * @private set cover img * @param {*} ref * @param {*} props */ const setCoverImg = (ref,props) => { const { width, height, coverBg } = props const ctx = getContext2d(ref,props) const img = new Image() img.src = coverBg ctx.drawImage(img, width, height) ctx.globalCompositeOperation = 'destination-out' } /** * @private set conver background color * @param {*} ref * @param {*} props */ const setCoverBgColor = (ref,props) => { const { coverBg, width, height } = props const ctx = getContext2d(ref,props) ctx.fillStyle = coverBg; ctx.fillRect(0, 0, width, height) ctx.globalCompositeOperation = 'destination-out' } /** * @public switch to set cover background color or set cover img * @param {*} ref * @param {*} props */ const setCover = (ref, props) => { const { coverBg } = props isLink(coverBg) ? setCoverImg(ref, props) : setCoverBgColor(ref, props) } export { getBg, getContext2d, setSize, setCover }
好现在我们可以这样使用了。如果不加参数的话默认会是一个黑色的块。
我这里设置了深绿色。如果想设置图片也可以。只要传一个链接就好了。现在看起来会是这个样子。
<ReactScratch baseBg={baseBg} coverBg="#006060" />
drawCircle就是字面意思画一个圆。这个圆是我们要用到的擦除效果。接收x,y坐标和size。size的话我们用ratioSize就好啦。于是乎我在utils.js里面加了一段。如果你跟着我的笔记走的话。不要忘记在export上把drawCircle暴露出去了噢。
/** * @public draw a cirle in (x,y) postion * @param {*} ref * @param {*} x * @param {*} y * @param {*} size */ const drawCircle = (ref, x, y, size) => { const ctx = getContext2d(ref) ctx.beginPath(); ctx.arc(x, y, size, 0, Math.PI * 2); ctx.fill() }
为了测试这个功能是否如期运行。我们就直接在componentDidMount里面加一段吧。目标是看见博士的脸。
componentDidMount
// ... import { getBg, getContext2d, setSize, setCover, drawCircle } from './utils' // ... componentDidMount () { const node = this.canvasRef.current setSize(node,this.props) setCover(node,this.props) const { width, height, ratioSize } = this.props const size = 30 const x = 255 const y = 55 drawCircle(node, x, y, ratioSize) }
看见了吗。
好看见了。说明一切如期运行着。那么我们把加进index.js的部分都删掉吧。 感觉我们现在就差一个事件监听了。那么接下来我们会开始创建我们的事件监听。
emmmm 我们要做的事件监听无非就是两个。
手机端会麻烦一点但是还好。
在最前面的最后一段我曾经说过这个。所以我们需要维护一个state。现在我把这个state弄出来了并把它叫做pressed。同时我添加了两个方法。一个是onPressed和onLoosen,意思分别是按压和松开的时候.
pressed
onPressed
onLoosen
export default class ReactScratch extends Component { constructor (props) { super(props) this.canvasRef = React.createRef() this.state = { pressed: false } this.onPressed = this.onPressed.bind(this) this.onLoosen = this.onLoosen.bind(this) } onPressed () { this.setState({ pressed: true }) } onLoosen () { this.setState({ pressed: false }) } // .... }
以上就是现在代码看起来的样子。接下来我们要知道在画布的哪个位置画圆。
我们先把事件赋给canvas。并添加一个叫做onMove的事件。这个事件会在我们按压移动的时候触发并且打印出坐标。
onMove
export default class ReactScratch extends Component { constructor (props) { super(props) this.canvasRef = React.createRef() this.state = { pressed: false } this.onPressed = this.onPressed.bind(this) this.onLoosen = this.onLoosen.bind(this) this.onMove = this.onMove.bind(this) } onPressed () { this.setState({ pressed: true }) } onLoosen () { this.setState({ pressed: false }) } onMove (e) { const { pressed } = this.state if (pressed) { const left = e.target.offsetLeft const top = e.target.offsetTop const x = e.pageX -left , y = e.pageY - top console.log(`x: ${x}, y: ${y}`) } } componentDidMount () { const node = this.canvasRef.current setSize(node,this.props) setCover(node,this.props) } render () { return ( <canvas style={{ border: '1px solid black', background: getBg(this.props), backgroundSize: 'cover' }} onMouseDown={this.onPressed} onMouseUp={this.onLoosen} onMouseMove={this.onMove} ref={this.canvasRef} /> ) } }
略微修改onMove。并让canvas绑定事件。
onMove (e) { const { pressed } = this.state if (pressed) { const { ratioSize } = this.props, left = e.target.offsetLeft, top = e.target.offsetTop, pageX = e.pageX || e.targetTouches[0].pageX, pageY = e.pageY || e.targetTouches[0].pageY, x = pageX - left - ratioSize / 2, y = pageY - top - ratioSize / 2 drawCircle(this.canvasRef.current, x, y, ratioSize) } } render () { return ( <canvas style={{ border: '1px solid black', background: getBg(this.props), backgroundSize: 'cover' }} onMouseDown={this.onPressed} onMouseUp={this.onLoosen} onMouseMove={this.onMove} onTouchStart={this.onPressed} onTouchEnd={this.onLoosen} onTouchMove={this.onMove} ref={this.canvasRef} /> ) }
现在的react-scratch已经可以实现擦除了。删掉border: '1px solid black'因为这个不是必要的只是我在开发的时候想让这个canvas显眼一点才添加的。
border: '1px solid black'
手机端
PC端
Ok,那么整个组件的开发就到这里结束了。
才怪呢。ratio的callback还没有做呢。那么问题来了怎么获取ratio...
说实话我不知道怎么获取。但我觉得应该是计算覆盖在底图上的α指数(透明程度)有多少吧。看了lucky-card的源码确认了这点。感谢lucky-card !! 这里贴一下lucky-card的实现。
function _forEach(items, callback) { return Array.prototype.forEach.call(items, function(item, idx) { callback(item, idx); }); } function _calcArea(ctx, callback, ratio) { var pixels = ctx.getImageData(0, 0, this.cWidth, this.cHeight); var transPixels = []; _forEach(pixels.data, function(item, i) { var pixel = pixels.data[i + 3]; if (pixel === 0) { transPixels.push(pixel); } }); if (transPixels.length / pixels.data.length > ratio) { callback && typeof callback === 'function' && callback(); } }
无耻的修改复制到utils.js里面。
/** * @public compute the ratio in canvas * @param {*} ref * @param {*} props */ const computeRatio = (ref, props) => { const { width, height } = props, pixels = getContext2d(ref).getImageData(0, 0, width, height), transPixels = [] pixels.data.forEach((item, i) => { const pixel = pixels.data[i + 3] if (pixel === 0) { transPixels.push(pixel) } }) return transPixels.length / pixels.data.length }
改动一下index.js里的onLoosen
onLoosen () { const node = this.canvasRef.current, { ratio, callback } = this.props, _ratio = computeRatio(node, this.props) if (_ratio >= ratio) { callback() } this.setState({ pressed: false }) }
理论上来说这个回调函数执行的次数应该是一次。但是这并不应该交由我来处理。应该给用户处理到底要执行多少次。
更改一下用法。加一个callback。让到达ratio的时候输出hello react scratch。
hello react scratch
<ReactScratch baseBg={baseBg} coverBg="#006060" callback={() => console.log('hello react scratch')} />
一切运行正常 。终于react-strach可以release v1.0.0了
虽然是个很小的组件。但是我认为笔记的描述程度有点过于多了。往后的笔记会看情况精简。
这个组件没有做测试,覆盖率很低。未来可能会补上。但是现在就到此为止吧。还有别的事情要做。
仓库地址: react-scratch 。欢(qiu qiu)迎(ni)给个star。
程序员的三大浪漫,编译原理,图形学,操作系统。最近在弄图形学相关的东西,emmmm , 说不上图形学。。。其实也就是在弄canvas。吃饭的时候看到支付宝付款后有一个刮刮卡。于是就在想能不能做个类似的东西呢。
答案是当然可以啊。于是就弄了一个demo。弄完之后又在想可不可以弄一个react的刮刮卡组件呢。当然也可以。这篇文章将记录我怎么弄一个这样的组件。
弄组件之前。我们先看看刮刮卡效果的实现原理。demo的代码很短加上空格也不到60行。
先看第一段。创建一个Image标签。并且获取canvas元素。为之后要用到这两个东西作准备。
接下来就是监听图片的load事件。当load完成后开始做某些事情。
除此以外就没有其他的代码了。后面的篇幅讲的都是load完之后做的事情。所以会省略掉上面那段代码。只贴里面的核心逻辑。
完成刮刮卡效果的最主要的核心是通过compositing operation来控制组合图形。详细的资料可以在这里查到。还不错的中文博客。
在刮刮卡里面用到了compositing operation的
destination-out
属性。destination-out
这是MDN对
destination-out
的描述。但是这段话翻译过来还是蛮抽象,蛮难理解的的。这里我用我们肉眼可见的效果。可观察的结果来说就是使用了destination-out
的图形。每当新的图形如果叠加在这个图形上的话。会让这个图形和新的图形叠加的部分变透明。这里我特意写了个demo展示了一下这个效果。如果你不理解我上面的文字描述可以看看。
实现擦除效果
如果知道了
destination-out
给我们带来的效果。那么实现一个擦除效果就很简单了。我们只需要简单的在一个图层上叠图形就好了。叠的图形的首选当然是圆形。因为圆形比较像人的手指。圆的大小可以自己决定。我们唯一要运算的东西只有一个,那就是x,y坐标。这两个坐标就是鼠标在画布里面的坐标。事件上的处理
一般来说刮刮卡 当然是要有一种刮开的感觉。当手指(鼠标)按住然后滑动才开始刮。松开后就停止刮。要实现这个逻辑的话也是非常简单。我们只需要维护一个state就好了。在开始时(mousedown)的时候把state设置为true。在松开时(mouseup)的时候把state设置为false。移(划)动(mouseup)的时候根据state判断是否开始擦除。
目标总结
总结一下。我们要做的事情就是实现擦除效果。处理手机和PC的事件。codepen的例子只处理了PC。所以在react-scrath中会把他补全。
开始
我使用了nwb构建项目。我写过一篇怎么构建项目的文章。可以看这里了解一下。但是这一篇不会告诉你怎么使用nwb。
我的目标和需求
为了写一个可以给其他人使用的sratch组件。那么我们需要定义一些可以定制化的东西。
我定了一下react-sratch需要接收的一些prop和它们的默认值。其中width,height,ratio的大小都是px。
代码上的实现
接下来要做的事情
首先我们设置一下canvas的大小并且把底图加到canvas上面吧。这里为了获得canvas的ref。用到了新的api-createRef()。可以了解一下。
我这里放弃思考图片要怎么样。一律都是
cover
。我也不管图片是什么结尾。反正是https或者http开头的我都当成是链接。现在可以这样使用。传一个地址或者一个颜色(可以是rgb,hsl)。
现在看起来是这样子。这里用的图是博士和坂本。
设置coverBg
好啊。设置完了这个。那就加一层遮罩(覆盖在上面的东东)吧。也就是coverBg。因为增加了一些api。整个
index.js
看起来有一些臃肿。所以我把设置canvas的东西抽出来放在了utils.js
里面。重用了一下isLink
函数。index.js
utils.js
好现在我们可以这样使用了。如果不加参数的话默认会是一个黑色的块。
我这里设置了深绿色。如果想设置图片也可以。只要传一个链接就好了。现在看起来会是这个样子。
增加drawCircle
drawCircle就是字面意思画一个圆。这个圆是我们要用到的擦除效果。接收x,y坐标和size。size的话我们用ratioSize就好啦。于是乎我在
utils.js
里面加了一段。如果你跟着我的笔记走的话。不要忘记在export上把drawCircle暴露出去了噢。为了测试这个功能是否如期运行。我们就直接在
componentDidMount
里面加一段吧。目标是看见博士的脸。看见了吗。
好看见了。说明一切如期运行着。那么我们把加进
index.js
的部分都删掉吧。 感觉我们现在就差一个事件监听了。那么接下来我们会开始创建我们的事件监听。emmmm 我们要做的事件监听无非就是两个。
手机端会麻烦一点但是还好。
pressed state
在最前面的最后一段我曾经说过这个。所以我们需要维护一个state。现在我把这个state弄出来了并把它叫做
pressed
。同时我添加了两个方法。一个是onPressed
和onLoosen
,意思分别是按压和松开的时候.以上就是现在代码看起来的样子。接下来我们要知道在画布的哪个位置画圆。
获得坐标位置
我们先把事件赋给canvas。并添加一个叫做
onMove
的事件。这个事件会在我们按压移动的时候触发并且打印出坐标。擦除
略微修改
onMove
。并让canvas绑定事件。现在的react-scratch已经可以实现擦除了。删掉
border: '1px solid black'
因为这个不是必要的只是我在开发的时候想让这个canvas显眼一点才添加的。手机端
PC端
Ok,那么整个组件的开发就到这里结束了。
才怪呢。ratio的callback还没有做呢。那么问题来了怎么获取ratio...
获取ratio
说实话我不知道怎么获取。但我觉得应该是计算覆盖在底图上的α指数(透明程度)有多少吧。看了lucky-card的源码确认了这点。感谢lucky-card !! 这里贴一下lucky-card的实现。
无耻的修改复制到
utils.js
里面。改动一下
index.js
里的onLoosen
理论上来说这个回调函数执行的次数应该是一次。但是这并不应该交由我来处理。应该给用户处理到底要执行多少次。
更改一下用法。加一个callback。让到达ratio的时候输出
hello react scratch
。一切运行正常 。终于react-strach可以release v1.0.0了
后记
虽然是个很小的组件。但是我认为笔记的描述程度有点过于多了。往后的笔记会看情况精简。
这个组件没有做测试,覆盖率很低。未来可能会补上。但是现在就到此为止吧。还有别的事情要做。
仓库地址: react-scratch 。欢(qiu qiu)迎(ni)给个star。