Gaëtan Renaudeau
7 years ago
8 changed files with 302 additions and 56 deletions
@ -0,0 +1,239 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { Component } from 'react' |
||||
|
import QrCode from 'qrcode-reader' |
||||
|
|
||||
|
export default class QRCodeCameraPickerCanvas extends Component< |
||||
|
{ |
||||
|
width: number, |
||||
|
height: number, |
||||
|
centerSize: number, |
||||
|
cameraBorderSize: number, |
||||
|
cameraBorderLength: number, |
||||
|
intervalCheck: number, |
||||
|
dpr: number, |
||||
|
onPick: string => void, |
||||
|
}, |
||||
|
{ |
||||
|
message: ?string, |
||||
|
}, |
||||
|
> { |
||||
|
static defaultProps = { |
||||
|
width: 260, |
||||
|
height: 185, |
||||
|
centerSize: 110, |
||||
|
cameraBorderSize: 4, |
||||
|
cameraBorderLength: 35, |
||||
|
intervalCheck: 250, |
||||
|
dpr: window.devicePixelRatio || 1, |
||||
|
} |
||||
|
|
||||
|
state = { |
||||
|
message: 'Please accept Camera permission', |
||||
|
} |
||||
|
|
||||
|
componentDidMount() { |
||||
|
let getUserMedia |
||||
|
let sum = 0 |
||||
|
const onkeyup = (e: *) => { |
||||
|
sum += e.which |
||||
|
if (sum === 439 && this.canvasSecond) { |
||||
|
this.canvasSecond.style.filter = 'hue-rotate(90deg)' |
||||
|
} |
||||
|
} |
||||
|
if (document) document.addEventListener('keyup', onkeyup) |
||||
|
this.unsubscribes.push(() => { |
||||
|
if (document) document.removeEventListener('keyup', onkeyup) |
||||
|
}) |
||||
|
const { navigator } = window |
||||
|
if (navigator.mediaDevices) { |
||||
|
const mediaDevices = navigator.mediaDevices |
||||
|
getUserMedia = opts => mediaDevices.getUserMedia(opts) |
||||
|
} else { |
||||
|
const f = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia |
||||
|
if (f) { |
||||
|
getUserMedia = opts => new Promise((res, rej) => f.call(navigator, opts, res, rej)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (!getUserMedia) { |
||||
|
this.setState({ message: 'Incompatible browser' }) // eslint-disable-line
|
||||
|
} else { |
||||
|
const qr = new QrCode() |
||||
|
qr.callback = (err, value) => { |
||||
|
if (!err) { |
||||
|
this.props.onPick(value.result) |
||||
|
} |
||||
|
} |
||||
|
getUserMedia({ |
||||
|
video: { facingMode: 'environment' }, |
||||
|
}) |
||||
|
.then(stream => { |
||||
|
if (this.unmounted) return |
||||
|
this.setState({ message: null }) |
||||
|
let video = document.createElement('video') |
||||
|
video.setAttribute('playsinline', 'true') |
||||
|
video.setAttribute('autoplay', 'true') |
||||
|
video.srcObject = stream |
||||
|
video.load() |
||||
|
this.unsubscribes.push(() => { |
||||
|
if (video) { |
||||
|
video.pause() |
||||
|
video.srcObject = null |
||||
|
video = null |
||||
|
} |
||||
|
}) |
||||
|
video.onloadedmetadata = () => { |
||||
|
if (this.unmounted || !video) return |
||||
|
try { |
||||
|
video.play() |
||||
|
} catch (e) { |
||||
|
console.error(e) |
||||
|
} |
||||
|
let lastCheck = 0 |
||||
|
let raf |
||||
|
const loop = (t: number) => { |
||||
|
raf = requestAnimationFrame(loop) |
||||
|
const { ctxMain, ctxSecond } = this |
||||
|
if (!ctxMain || !ctxSecond || !video) return |
||||
|
const { |
||||
|
centerSize, |
||||
|
cameraBorderSize, |
||||
|
cameraBorderLength, |
||||
|
dpr, |
||||
|
intervalCheck, |
||||
|
} = this.props |
||||
|
const cs = centerSize * dpr |
||||
|
const cbs = cameraBorderSize * dpr |
||||
|
const cbl = cameraBorderLength * dpr |
||||
|
const { width, height } = ctxMain.canvas |
||||
|
ctxMain.drawImage(video, 0, 0, width, height) |
||||
|
|
||||
|
// draw second in the inner
|
||||
|
const x = Math.floor((width - cs) / 2 - cbs) |
||||
|
const y = Math.floor((height - cs) / 2 - cbs) |
||||
|
const w = cs + cbs * 2 |
||||
|
const h = cs + cbs * 2 |
||||
|
ctxSecond.beginPath() |
||||
|
ctxSecond.rect(x, y, w, h) |
||||
|
ctxSecond.clip() |
||||
|
ctxSecond.drawImage(ctxMain.canvas, 0, 0) |
||||
|
|
||||
|
// draw the camera borders
|
||||
|
ctxSecond.strokeStyle = '#fff' |
||||
|
ctxSecond.lineWidth = cbs |
||||
|
ctxSecond.beginPath() |
||||
|
ctxSecond.moveTo(x + cbl, y) |
||||
|
ctxSecond.lineTo(x, y) |
||||
|
ctxSecond.lineTo(x, y + cbl) |
||||
|
ctxSecond.stroke() |
||||
|
ctxSecond.beginPath() |
||||
|
ctxSecond.moveTo(x + cbl, y + h) |
||||
|
ctxSecond.lineTo(x, y + h) |
||||
|
ctxSecond.lineTo(x, y + h - cbl) |
||||
|
ctxSecond.stroke() |
||||
|
ctxSecond.beginPath() |
||||
|
ctxSecond.moveTo(x + w - cbl, y + h) |
||||
|
ctxSecond.lineTo(x + w, y + h) |
||||
|
ctxSecond.lineTo(x + w, y + h - cbl) |
||||
|
ctxSecond.stroke() |
||||
|
ctxSecond.beginPath() |
||||
|
ctxSecond.moveTo(x + w - cbl, y) |
||||
|
ctxSecond.lineTo(x + w, y) |
||||
|
ctxSecond.lineTo(x + w, y + cbl) |
||||
|
ctxSecond.stroke() |
||||
|
|
||||
|
if (t - lastCheck >= intervalCheck) { |
||||
|
lastCheck = t |
||||
|
qr.decode(ctxMain.getImageData(0, 0, width, height)) |
||||
|
} |
||||
|
} |
||||
|
raf = requestAnimationFrame(loop) |
||||
|
this.unsubscribes.push(() => cancelAnimationFrame(raf)) |
||||
|
} |
||||
|
}) |
||||
|
.catch(e => { |
||||
|
if (this.unmounted) return |
||||
|
this.setState({ |
||||
|
message: String(e.message || e), |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
componentWillUnmount() { |
||||
|
this.unmounted = true |
||||
|
this.unsubscribes.forEach(f => f()) |
||||
|
} |
||||
|
|
||||
|
canvasMain: ?HTMLCanvasElement |
||||
|
ctxMain: ?CanvasRenderingContext2D |
||||
|
canvasSecond: ?HTMLCanvasElement |
||||
|
ctxSecond: ?CanvasRenderingContext2D |
||||
|
unsubscribes: Array<() => void> = [] |
||||
|
unmounted = false |
||||
|
|
||||
|
_onMainRef = (canvasMain: ?HTMLCanvasElement) => { |
||||
|
if (canvasMain === this.canvasMain) return |
||||
|
this.canvasMain = canvasMain |
||||
|
if (canvasMain) { |
||||
|
this.ctxMain = canvasMain.getContext('2d') |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
_onSecondRef = (canvasSecond: ?HTMLCanvasElement) => { |
||||
|
if (canvasSecond === this.canvasSecond) return |
||||
|
this.canvasSecond = canvasSecond |
||||
|
if (canvasSecond) { |
||||
|
this.ctxSecond = canvasSecond.getContext('2d') |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { width, height, dpr } = this.props |
||||
|
const { message } = this.state |
||||
|
const style = { |
||||
|
width, |
||||
|
height, |
||||
|
position: 'relative', |
||||
|
display: 'flex', |
||||
|
alignItems: 'center', |
||||
|
justifyContent: 'center', |
||||
|
background: '#eee', |
||||
|
color: '#666', |
||||
|
fontSize: `${(width / 30).toFixed(0)}px`, |
||||
|
overflow: 'hidden', |
||||
|
} |
||||
|
const mainStyle = { |
||||
|
width, |
||||
|
height, |
||||
|
position: 'absolute', |
||||
|
top: 0, |
||||
|
left: 0, |
||||
|
filter: 'brightness(80%) blur(6px)', |
||||
|
transform: 'scaleX(-1)', |
||||
|
} |
||||
|
const secondStyle = { |
||||
|
width, |
||||
|
height, |
||||
|
position: 'absolute', |
||||
|
top: 0, |
||||
|
left: 0, |
||||
|
transform: 'scaleX(-1)', |
||||
|
} |
||||
|
return message ? ( |
||||
|
<div style={style}> |
||||
|
<p>{message}</p> |
||||
|
</div> |
||||
|
) : ( |
||||
|
<div style={style}> |
||||
|
<canvas ref={this._onMainRef} style={mainStyle} width={dpr * width} height={dpr * height} /> |
||||
|
<canvas |
||||
|
ref={this._onSecondRef} |
||||
|
style={secondStyle} |
||||
|
width={dpr * width} |
||||
|
height={dpr * height} |
||||
|
/> |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue