Commit 3db472a4 authored by wind.wang's avatar wind.wang

init

parents
const fs = require('fs');
const path = require('path');
const parse5 = require('parse5');
const flow = require('lodash/fp/flow');
const babel = require('@babel/core');
const Node = {
map: transform => node => {
const transformed = transform(node);
const {childNodes} = transformed;
return {
...transformed,
childNodes: childNodes && childNodes.map(Node.map(transform)),
};
},
};
const ENTRY = process.argv[2];
const entryPath = require.resolve(ENTRY);
const entryContent = fs.readFileSync(entryPath, 'utf-8');
const newContent = flow([
parse5.parse,
Node.map(node => {
if (node.nodeName === 'script') {
const src = node.attrs.find(attr => attr.name === 'src');
if (src.value) {
const scriptPath = path.resolve(path.dirname(entryPath), src.value);
const scriptContent = babel.transform(fs.readFileSync(scriptPath, 'utf-8'), {
comments: false,
}).code;
const [newScript] = parse5.parseFragment(`<script>${scriptContent}</script>`, node.parent).childNodes;
return newScript;
}
}
return node;
}),
parse5.serialize,
])(entryContent);
fs.writeFileSync(`${entryPath}.js`, `export default \`${newContent}\``);
console.log(`${ENTRY} -> ${ENTRY}.js`);
{
"presets": ["babel-preset-expo"],
"env": {
"development": {
"plugins": ["transform-react-jsx-source"]
}
}
}
# See https://help.github.com/ignore-files/ for more about ignoring files.
# expo
.expo/
# dependencies
/node_modules
# misc
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
import React, {Component} from 'react';
import {Image, ScrollView, StatusBar, Text, View, StyleSheet} from 'react-native';
import Canvas, {Image as CanvasImage, Path2D, ImageData} from 'react-native-canvas';
const Example = ({sample, children}) => (
<View style={styles.example}>
<View style={styles.exampleLeft}>{children}</View>
<View style={styles.exampleRight}>
<Image source={sample} style={{width: 100, height: 100}} />
</View>
</View>
);
export default class App extends Component {
handleImageData(canvas) {
canvas.width = 100;
canvas.height = 100;
const context = canvas.getContext('2d');
context.fillStyle = 'purple';
context.fillRect(0, 0, 100, 100);
context.getImageData(0, 0, 100, 100)
.then(imageData => {
const data = Object.values(imageData.data);
const length = Object.keys(data).length;
for (let i = 0; i < length; i += 4) {
data[i] = 0;
data[i + 1] = 0;
data[i + 2] = 0;
}
const imgData = new ImageData(canvas, data, 100, 100);
context.putImageData(imgData, 0, 0);
});
}
async handlePurpleRect(canvas) {
canvas.width = 100;
canvas.height = 100;
const context = canvas.getContext('2d');
context.fillStyle = 'purple';
context.fillRect(0, 0, 100, 100);
const {width} = await context.measureText('yo');
}
handleRedCircle(canvas) {
canvas.width = 100;
canvas.height = 100;
const context = canvas.getContext('2d');
context.fillStyle = 'red';
context.arc(50, 50, 49, 0, Math.PI * 2, true);
context.fill();
}
handleImageRect(canvas) {
const image = new CanvasImage(canvas);
canvas.width = 100;
canvas.height = 100;
const context = canvas.getContext('2d');
image.src = 'https://image.freepik.com/free-vector/unicorn-background-design_1324-79.jpg';
image.addEventListener('load', () => {
context.drawImage(image, 0, 0, 100, 100);
});
}
handlePath(canvas) {
canvas.width = 100;
canvas.height = 100;
const context = canvas.getContext('2d');
context.fillStyle = 'red';
context.fillRect(0, 0, 100, 100);
const ellipse = new Path2D(canvas);
ellipse.ellipse(50, 50, 25, 35, (45 * Math.PI) / 180, 0, 2 * Math.PI);
context.fillStyle = 'purple';
context.fill(ellipse);
context.save();
context.scale(0.5, 0.5);
context.translate(50, 20);
const rectPath = new Path2D(canvas, 'M10 10 h 80 v 80 h -80 Z');
context.fillStyle = 'pink';
context.fill(rectPath);
context.restore();
}
async handleGradient(canvas) {
canvas.width = 100;
canvas.height = 100;
const ctx = canvas.getContext('2d');
const gradient = await ctx.createLinearGradient(0, 0, 200, 0);
gradient.addColorStop(0, 'green');
gradient.addColorStop(1, 'white');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 100, 100);
}
/**
* Extracted from https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations
*/
handlePanorama(canvas) {
const CanvasXSize = 100;
const CanvasYSize = 100;
canvas.width = CanvasXSize;
canvas.height = CanvasYSize;
const ctx = canvas.getContext('2d');
const img = new CanvasImage(canvas);
// User Variables - customize these to change the image being scrolled, its
// direction, and the speed.
img.src = 'https://mdn.mozillademos.org/files/4553/Capitan_Meadows,_Yosemite_National_Park.jpg';
const speed = 30; // lower is faster
const scale = 1.05;
const y = -4.5; // vertical offset
// Main program
const dx = 0.75;
let imgW;
let imgH;
let x = 0;
let clearX;
let clearY;
img.addEventListener('load', () => {
imgW = img.width * scale;
imgH = img.height * scale;
if (imgW > CanvasXSize) {
x = CanvasXSize - imgW;
} // image larger than canvas
if (imgW > CanvasXSize) {
clearX = imgW;
} else {
// image width larger than canvas
clearX = CanvasXSize;
}
if (imgH > CanvasYSize) {
clearY = imgH;
} else {
// image height larger than canvas
clearY = CanvasYSize;
}
// set refresh rate
return setInterval(draw, speed);
});
function draw() {
ctx.clearRect(0, 0, clearX, clearY); // clear the canvas
// if image is <= Canvas Size
if (imgW <= CanvasXSize) {
// reset, start from beginning
if (x > CanvasXSize) {
x = -imgW + x;
}
// draw additional image1
if (x > 0) {
ctx.drawImage(img, -imgW + x, y, imgW, imgH);
}
// draw additional image2
if (x - imgW > 0) {
ctx.drawImage(img, -imgW * 2 + x, y, imgW, imgH);
}
} else {
// if image is > Canvas Size
// reset, start from beginning
if (x > CanvasXSize) {
x = CanvasXSize - imgW;
}
// draw additional image
if (x > CanvasXSize - imgW) {
ctx.drawImage(img, x - imgW + 1, y, imgW, imgH);
}
}
// draw image
ctx.drawImage(img, x, y, imgW, imgH);
// amount to move
x += dx;
}
}
handleEmbedHTML(canvas) {
const image = new CanvasImage(canvas);
canvas.width = 100;
canvas.height = 100;
const context = canvas.getContext('2d');
const htmlString = '<b>Hello, World!</b>';
const svgString = `
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<foreignObject width="100%" height="100%">
<div xmlns="http://www.w3.org/1999/xhtml" style="font-size: 40px; background: lightblue; width: 100vw; height: 100vh;">
<span style="background: pink;">
${htmlString}
</span>
</div>
</foreignObject>
</svg>
`;
image.src = `data:image/svg+xml,${encodeURIComponent(svgString)}`;
image.addEventListener('load', () => {
context.drawImage(image, 0, 0, 100, 100);
});
}
render() {
return (
<View style={styles.container}>
<StatusBar hidden={true} />
<ScrollView style={styles.examples}>
<Example sample={require('./images/purple-black-rect.png')}>
<Canvas ref={this.handleImageData} />
</Example>
<Example sample={require('./images/purple-rect.png')}>
<Canvas ref={this.handlePurpleRect} />
</Example>
<Example sample={require('./images/red-circle.png')}>
<Canvas ref={this.handleRedCircle} />
</Example>
<Example sample={require('./images/image-rect.png')}>
<Canvas ref={this.handleImageRect} />
</Example>
<Example sample={require('./images/path.png')}>
<Canvas ref={this.handlePath} />
</Example>
<Example sample={require('./images/gradient.png')}>
<Canvas ref={this.handleGradient} />
</Example>
<Example sample={require('./images/panorama.png')}>
<Canvas ref={this.handlePanorama} />
</Example>
<Example sample={require('./images/embed-html.png')}>
<Canvas ref={this.handleEmbedHTML} />
</Example>
</ScrollView>
</View>
);
}
}
const full = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
};
const cell = {
flex: 1,
padding: 10,
justifyContent: 'center',
alignItems: 'center',
};
const styles = StyleSheet.create({
container: {
...full,
},
examples: {
...full,
padding: 5,
paddingBottom: 0,
},
example: {
paddingBottom: 5,
flex: 1,
flexDirection: 'row',
},
exampleLeft: {
...cell,
},
exampleRight: {
...cell,
},
});
import React from 'react';
import App from './App';
import renderer from 'react-test-renderer';
it('renders without crashing', () => {
const rendered = renderer.create(<App />).toJSON();
expect(rendered).toBeTruthy();
});
This diff is collapsed.
{
"expo": {
"sdkVersion": "27.0.0"
}
}
{
"name": "example",
"version": "0.1.0",
"private": true,
"devDependencies": {
"jest-expo": "~27.0.0",
"react-native-scripts": "1.14.0",
"react-test-renderer": "16.3.1"
},
"main": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
"scripts": {
"start": "react-native-scripts start",
"eject": "react-native-scripts eject",
"android": "react-native-scripts android",
"ios": "react-native-scripts ios",
"test": "jest"
},
"jest": {
"preset": "jest-expo"
},
"dependencies": {
"expo": "^27.0.1",
"react": "16.3.1",
"react-native": "~0.55.2",
"react-native-canvas": "^0.1.22"
}
}
This diff is collapsed.
{
"name": "react-native-canvas",
"license": "MIT",
"version": "0.1.26",
"main": "dist/Canvas.js",
"scripts": {
"build": "babel src --out-dir dist --copy-files --compact false && node bundle-html.js ./dist/index.html",
"copy-to-example": "rsync -rv dist example/node_modules/react-native-canvas",
"build-to-example": "npm run build; npm run copy-to-example;",
"prepare": "npm run build"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.47",
"@babel/core": "7.0.0-beta.47",
"@babel/plugin-proposal-decorators": "7.0.0-beta.47",
"babel-preset-react-native": "^5.0.2",
"eslint": "^3",
"eslint-config-fbjs-opensource": "^1.0.0",
"parse5": "^5.0.0",
"prettier": "^1.14.2"
},
"dependencies": {
"ctx-polyfill": "^1.1.4",
"react-native-webview": "^5.2.0"
},
"repository": "https://github.com/iddan/react-native-canvas"
}
<div align="center">
<img src="https://emojipedia-us.s3.amazonaws.com/thumbs/240/apple/96/fireworks_1f386.png"/>
<h1>react-native-canvas</h1>
</div>
A Canvas component for React Native
```bash
npm install react-native-canvas
```
### Usage
```JSX
import React, { Component } from 'react';
import Canvas from 'react-native-canvas';
class App extends Component {
handleCanvas = (canvas) => {
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'purple';
ctx.fillRect(0, 0, 100, 100);
}
render() {
return (
<Canvas ref={this.handleCanvas}/>
)
}
}
```
### API
#### Canvas
###### Canvas#height
Reflects the height of the canvas in pixels
###### Canvas#width
Reflects the width of the canvas in pixels
###### Canvas#getContext()
Returns a canvas rendering context. Currently only supports 2d context.
###### Canvas#toDataURL()
Returns a `Promise` that resolves to DataURL.
#### CanvasRenderingContext2D
Standard CanvasRenderingContext2D. [MDN](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D). Only difference is `await` should be used to retrieve values from methods.
```javascript
const ctx = canvas.getContext('2d');
```
#### Image
WebView Image constructor. Unlike in the browsers accepts canvas as first argument. [MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/Image)
```javascript
const image = new Image(canvas, height, width);
```
/**
* @typedef {Object} Message
* @property {string} id
*/
export default class Bus {
_paused = false;
/**
* @type {Object.<string, function>}
*/
_messageListeners = {};
/**
* @type {Message[]}
*/
_queue = [];
/**
* @param {function} send
*/
constructor(send) {
this._send = send;
}
/**
* @param {Message} message
* @return {Promise.<Message>}
*/
post(message) {
return new Promise(resolve => {
this._messageListeners[message.id] = resolve;
if (!this._paused) {
this._send(message);
} else {
this._queue.push(message);
}
});
}
/**
* @param {Message} message
* @return {void}
*/
handle(message) {
this._messageListeners[message.id](message);
}
/**
* @returns {void}
*/
pause() {
this._paused = true;
}
/**
* @returns {void}
*/
resume() {
this._paused = false;
this._send(this._queue);
this._queue = [];
}
}
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {View, Platform, ViewStylePropTypes} from 'react-native';
import {WebView} from 'react-native-webview';
import Bus from './Bus';
import {webviewTarget, webviewProperties, webviewMethods, constructors, WEBVIEW_TARGET} from './webview-binders';
import CanvasRenderingContext2D from './CanvasRenderingContext2D';
import html from './index.html.js';
export {default as Image} from './Image';
export {default as ImageData} from './ImageData';
export {default as Path2D} from './Path2D';
import './CanvasGradient';
@webviewTarget('canvas')
@webviewProperties({width: 300, height: 150})
@webviewMethods(['toDataURL'])
export default class Canvas extends Component {
static propTypes = {
style: PropTypes.shape(ViewStylePropTypes),
baseUrl: PropTypes.string,
originWhitelist: PropTypes.arrayOf(PropTypes.string),
};
addMessageListener = listener => {
this.listeners.push(listener);
return () => this.removeMessageListener(listener);
};
removeMessageListener = listener => {
this.listeners.splice(this.listeners.indexOf(listener), 1);
};
loaded = false;
/**
* in the mounting process this.webview can be set to null
*/
webviewPostMessage = message => this.webview && this.webview.postMessage(JSON.stringify(message));
bus = new Bus(this.webviewPostMessage);
listeners = [];
context2D = new CanvasRenderingContext2D(this);
constructor() {
super();
this.bus.pause();
}
getContext = (contextType, contextAttributes) => {
switch (contextType) {
case '2d': {
return this.context2D;
}
}
return null;
};
postMessage = async message => {
const {stack} = new Error();
const {type, payload} = await this.bus.post({id: Math.random(), ...message});
switch (type) {
case 'error': {
const error = new Error(payload.message);
error.stack = stack;
throw error;
}
case 'json': {
return payload;
}
case 'blob': {
return atob(payload);
}
}
};
handleMessage = e => {
let data = JSON.parse(e.nativeEvent.data);
switch (data.type) {
case 'log': {
// eslint-disable-line no-console
console.log(...data.payload);
break;
}
case 'error': {
throw new Error(data.payload.message);
}
default: {
if (data.payload) {
const constructor = constructors[data.meta.constructor];
if (constructor) {
const {args, payload} = data;
data = {
...data,
payload: Object.assign(new constructor(this, ...args), payload, {[WEBVIEW_TARGET]: data.meta.target}),
};
}
for (const listener of this.listeners) {
listener(data.payload);
}
}
this.bus.handle(data);
}
}
};
handleRef = element => {
this.webview = element;
};
handleLoad = () => {
this.loaded = true;
this.bus.resume();
};
render() {
const {width, height} = this;
const {style, baseUrl = '', originWhitelist = ['*']} = this.props;
if (Platform.OS === 'android') {
return (
<View style={{width, height, overflow: 'hidden', flex: 0, ...style}}>
<WebView
ref={this.handleRef}
style={{width, height, overflow: 'hidden', backgroundColor: 'transparent'}}
source={{html, baseUrl}}
originWhitelist={originWhitelist}
onMessage={this.handleMessage}
onLoad={this.handleLoad}
mixedContentMode="always"
scalesPageToFit={false}
javaScriptEnabled
domStorageEnabled
thirdPartyCookiesEnabled
allowUniversalAccessFromFileURLs
/>
</View>
);
}
return (
<View style={{width, height, overflow: 'hidden', flex: 0, ...style}}>
<WebView
ref={this.handleRef}
style={{width, height, overflow: 'hidden', backgroundColor: 'transparent'}}
source={{html, baseUrl}}
originWhitelist={originWhitelist}
onMessage={this.handleMessage}
onLoad={this.handleLoad}
scrollEnabled={false}
scalesPageToFit={false}
/>
</View>
);
}
}
import {webviewConstructor, webviewMethods} from './webview-binders';
@webviewMethods(['addColorStop'])
@webviewConstructor('CanvasGradient')
export default class CanvasGradient {
constructor(canvas) {
this.canvas = canvas;
}
postMessage = message => {
return this.canvas.postMessage(message);
};
}
import {webviewTarget, webviewProperties, webviewMethods} from './webview-binders';
@webviewTarget('context2D')
@webviewProperties({
fillStyle: '#000',
font: '10px sans-serif',
globalAlpha: 1.0,
globalCompositeOperation: 'source-over',
lineCap: 'butt',
lineDashOffset: 0.0,
lineJoin: 'miter',
lineWidth: 1.0,
miterLimit: 10.0,
shadowBlur: 0,
shadowColor: 'rgba(0,0,0,0)',
shadowOffsetX: 0,
shadowOffsetY: 0,
strokeStyle: '#000',
textAlign: 'start',
textBaseline: 'alphabetic',
})
@webviewMethods([
'arc',
'arcTo',
'beginPath',
'bezierCurveTo',
'clearRect',
'clip',
'closePath',
'createImageData',
'createLinearGradient',
'createPattern',
'createRadialGradient',
'drawFocusIfNeeded',
'drawImage',
'drawWidgetAsOnScreen',
'drawWindow',
'fill',
'fillRect',
'fillText',
'getImageData',
'getLineDash',
'isPointInPath',
'isPointInStroke',
'lineTo',
'measureText',
'moveTo',
'putImageData',
'quadraticCurveTo',
'rect',
'restore',
'rotate',
'save',
'scale',
'setLineDash',
'setTransform',
'stroke',
'strokeRect',
'strokeText',
'transform',
'translate',
])
export default class CanvasRenderingContext2D {
constructor(canvas) {
this.canvas = canvas;
}
postMessage(message) {
return this.canvas.postMessage(message);
}
}
import Canvas from './Canvas';
import {webviewConstructor, webviewProperties, webviewEvents} from './webview-binders';
@webviewProperties({crossOrigin: undefined, height: undefined, src: undefined, width: undefined})
@webviewEvents(['load', 'error'])
@webviewConstructor('Image')
export default class Image {
constructor(canvas, width, height) {
if (!(canvas instanceof Canvas)) {
throw new Error('Image must be initialized with a Canvas instance');
}
this.canvas = canvas;
if (this.onConstruction) {
this.onConstruction();
}
if (this.width) {
this.width = width;
}
if (this.height) {
this.height = height;
}
}
postMessage = message => {
return this.canvas.postMessage(message);
};
addMessageListener = listener => {
return this.canvas.addMessageListener(listener);
};
}
import Canvas from './Canvas';
import {webviewConstructor} from './webview-binders';
@webviewConstructor('ImageData')
export default class ImageData {
constructor(canvas, array, width, height) {
if (!(canvas instanceof Canvas)) {
throw new Error('ImageData must be initialized with a Canvas instance');
}
this.canvas = canvas;
if (this.onConstruction) {
this.onConstruction(array, width, height);
}
}
postMessage = message => {
return this.canvas.postMessage(message);
};
addMessageListener = listener => {
return this.canvas.addMessageListener(listener);
};
}
import {webviewConstructor, webviewMethods} from './webview-binders';
/**
* Currently doesn't support passing an SVGMatrix in addPath as SVGMatrix is deprecated
*/
@webviewMethods([
'addPath',
'closePath',
'moveTo',
'lineTo',
'bezierCurveTo',
'quadraticCurveTo',
'arc',
'arcTo',
'ellipse',
'rect',
])
@webviewConstructor('Path2D')
export default class Path2D {
constructor(canvas, ...args) {
this.canvas = canvas;
if (this.onConstruction) {
this.onConstruction(...args);
}
}
postMessage = message => {
return this.canvas.postMessage(message);
};
addMessageListener = listener => {
return this.canvas.addMessageListener(listener);
};
}
const scale = ratio => item => {
if (typeof item === 'number') {
return item * ratio;
}
return item;
};
/**
* Extracted from https://github.com/component/autoscale-canvas
* @param {Canvas} canvas
* @return {Canvas}
*/
window.autoScaleCanvas = function autoScaleCanvas(canvas) {
const ctx = canvas.getContext('2d');
const ratio = window.devicePixelRatio || 1;
if (ratio != 1) {
canvas.style.width = canvas.width + 'px';
canvas.style.height = canvas.height + 'px';
canvas.width *= ratio;
canvas.height *= ratio;
ctx.scale(ratio, ratio);
ctx.isPointInPath = (...args) =>
CanvasRenderingContext2D.prototype.isPointInPath.apply(ctx, args.map(scale(ratio)));
}
return canvas;
};
<html>
<head>
<meta content='width=device-width, initial-scale=1, maximum-scale=1, user-scaleable=no' name='viewport'>
<style>
body {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<script src='../node_modules/ctx-polyfill/ctx-polyfill.js' type='text/javascript'></script>
<script src="./autoscale-canvas.js"></script>
<script src="./webview.js"></script>
</body>
</html>
export const WEBVIEW_TARGET = '@@WEBVIEW_TARGET';
/**
* @mutable
*/
export const constructors = {};
export const webviewTarget = targetName => target => {
target.prototype[WEBVIEW_TARGET] = targetName;
};
const ID = () =>
Math.random()
.toString(32)
.slice(2);
/**
* These are where objects need other objects as an argument.
* Because when the data is sent as JSON it removes the class.
*
* One example being ImageData which requires the Uint8ClampedArray
* object as the first parameter.
*/
const SPECIAL_CONSTRUCTOR = {
ImageData: {
className: 'Uint8ClampedArray',
paramNum: 0,
},
};
export const webviewConstructor = constructorName => target => {
const {onConstruction} = target.prototype;
constructors[constructorName] = target;
/**
* Arguments should be identical to the arguments passed to the constructor
* just without the canvas instance
*/
target.prototype.onConstruction = function(...args) {
if (onConstruction) {
onConstruction.call(this);
}
if (SPECIAL_CONSTRUCTOR[constructorName] !== undefined) {
const {className, paramNum} = SPECIAL_CONSTRUCTOR[constructorName];
args[paramNum] = {className, classArgs: [args[paramNum]]};
}
this[WEBVIEW_TARGET] = ID();
this.postMessage({
type: 'construct',
payload: {
constructor: constructorName,
id: this[WEBVIEW_TARGET],
args,
},
});
};
target.prototype.toJSON = function() {
return {__ref__: this[WEBVIEW_TARGET]};
};
};
export const webviewMethods = methods => target => {
for (const method of methods) {
target.prototype[method] = function(...args) {
return this.postMessage({
type: 'exec',
payload: {
target: this[WEBVIEW_TARGET],
method,
args,
},
});
};
}
};
export const webviewProperties = properties => target => {
for (const key of Object.keys(properties)) {
const initialValue = properties[key];
const privateKey = `__${key}__`;
target.prototype[privateKey] = initialValue;
Object.defineProperty(target.prototype, key, {
get() {
return this[privateKey];
},
set(value) {
this.postMessage({
type: 'set',
payload: {
target: this[WEBVIEW_TARGET],
key,
value,
},
});
if (this.forceUpdate) {
this.forceUpdate();
}
return (this[privateKey] = value);
},
});
}
};
export const webviewEvents = types => target => {
const {onConstruction} = target.prototype;
target.prototype.onConstruction = function() {
if (onConstruction) {
onConstruction.call(this);
}
this.postMessage({
type: 'listen',
payload: {
types,
target: this[WEBVIEW_TARGET],
},
});
};
target.prototype.addEventListener = function(type, callback) {
this.addMessageListener(message => {
if (
message &&
message.type === 'event' &&
message.payload.target[WEBVIEW_TARGET] === this[WEBVIEW_TARGET] &&
message.payload.type === type
) {
for (const key in message.payload.target) {
const value = message.payload.target[key];
if (key in this && this[key] !== value) {
this[key] = value;
}
}
callback({
...message.payload,
target: this,
});
}
});
};
};
const WEBVIEW_TARGET = '@@WEBVIEW_TARGET';
const ID = () =>
Math.random()
.toString(32)
.slice(2);
const flattenObject = object => {
if (typeof object !== 'object') {
return object;
}
const flatObject = {};
for (const key in object) {
flatObject[key] = object[key];
}
for (const key in Object.getOwnPropertyNames(object)) {
flatObject[key] = object[key];
}
return flatObject;
};
class AutoScaledCanvas {
constructor(element) {
this.element = element;
}
toDataURL(...args) {
return this.element.toDataURL(...args);
}
autoScale() {
if (this.savedHeight !== undefined) {
this.element.height = this.savedHeight;
}
if (this.savedWidth !== undefined) {
this.element.width = this.savedWidth;
}
window.autoScaleCanvas(this.element);
}
get width() {
return this.element.width;
}
set width(value) {
this.savedWidth = value;
this.autoScale();
return value;
}
get height() {
return this.element.height;
}
set height(value) {
this.savedHeight = value;
this.autoScale();
return value;
}
}
const toMessage = result => {
if (result instanceof Blob) {
return {
type: 'blob',
payload: btoa(result),
meta: {},
};
}
if (result instanceof Object) {
if (!result[WEBVIEW_TARGET]) {
const id = ID();
result[WEBVIEW_TARGET] = id;
targets[id] = result;
}
return {
type: 'json',
payload: flattenObject(result),
args: toArgs(flattenObject(result)),
meta: {
target: result[WEBVIEW_TARGET],
constructor: result.__constructorName__ || result.constructor.name,
},
};
}
return {
type: 'json',
payload: JSON.stringify(result),
meta: {},
};
};
/**
* Gets the all the args required for creating the object.
* Also converts typed arrays to normal arrays.
*
* For example with ImageData we need a Uint8ClampedArray,
* but if we sent it as JSON it will be sent as an object
* not an array. So we convert any typed arrays into arrays
* first, they will be converted to Uint8ClampedArrays in
* `webview-binders.js`.
*
*/
const toArgs = result => {
const args = [];
for (const key in result) {
if (result[key] !== undefined && key !== '@@WEBVIEW_TARGET') {
if (typedArrays[result[key].constructor.name] !== undefined) {
result[key] = Array.from(result[key]);
}
args.push(result[key]);
}
}
return args;
};
/**
* Creates objects from args. If any argument have the object
* which contains `className` it means we need to convert that
* argument into an object.
*
* We need to do this because when we pass data between the WebView
* and RN using JSON, it strips/removes the class data from the object.
* So this will raise errors as the WebView will expect arguments to be
* of a certain class.
*
* For example for ImageData we expect to receive
* [{className: Uint8ClampedArray, classArgs: [Array(4)]}, 100, 100]
* We need to convert the first parameter into an object first.
*
*/
const createObjectsFromArgs = args => {
for (let index = 0; index < args.length; index += 1) {
const currentArg = args[index];
if (currentArg.className !== undefined) {
const {className, classArgs} = currentArg;
const constructor = new constructors[className](...classArgs);
args[index] = constructor;
}
}
return args;
};
// const print = (...args) => {
// const a = JSON.stringify({
// type: 'log',
// payload: args,
// });
// postMessage(a);
// };
const canvas = document.createElement('canvas');
const autoScaledCanvas = new AutoScaledCanvas(canvas);
const targets = {
canvas: autoScaledCanvas,
context2D: canvas.getContext('2d'),
};
const constructors = {
Image,
Path2D,
CanvasGradient,
ImageData,
Uint8ClampedArray,
};
const typedArrays = {
Uint8ClampedArray,
};
/**
* In iOS 9 constructors doesn't have bind defined which fails
* Babel object constructors utility function
*/
Image.bind =
Image.bind ||
function() {
return Image;
};
Path2D.bind =
Path2D.bind ||
function() {
return Path2D;
};
ImageData.bind =
ImageData.bind ||
function() {
return ImageData;
};
Uint8ClampedArray.bind =
Uint8ClampedArray.bind ||
function() {
return Uint8ClampedArray;
};
const populateRefs = arg => {
if (arg && arg.__ref__) {
return targets[arg.__ref__];
}
return arg;
};
document.body.appendChild(canvas);
function handleMessage({id, type, payload}) {
switch (type) {
case 'exec': {
const {target, method, args} = payload;
const result = targets[target][method](...args.map(populateRefs));
const message = toMessage(result);
/**
* In iOS 9 some classes name are not defined so we compare to
* known constructors to find the name.
*/
if (typeof result === 'object' && !message.meta.constructor) {
for (const constructorName in constructors) {
if (result instanceof constructors[constructorName]) {
message.meta.constructor = constructorName;
}
}
}
postMessage(JSON.stringify({id, ...message}));
break;
}
case 'set': {
const {target, key, value} = payload;
targets[target][key] = populateRefs(value);
break;
}
case 'construct': {
const {constructor, id: target, args = []} = payload;
const newArgs = createObjectsFromArgs(args);
const object = new constructors[constructor](...newArgs);
object.__constructorName__ = constructor;
const message = toMessage({});
targets[target] = object;
postMessage(JSON.stringify({id, ...message}));
break;
}
case 'listen': {
const {types, target} = payload;
for (const eventType of types) {
targets[target].addEventListener(eventType, e => {
const message = toMessage({
type: 'event',
payload: {
type: e.type,
target: {...flattenObject(targets[target]), [WEBVIEW_TARGET]: target},
},
});
postMessage(JSON.stringify({id, ...message}));
});
}
break;
}
}
}
const handleError = (err, message) => {
postMessage(
JSON.stringify({
id: message.id,
type: 'error',
payload: {
message: err.message,
},
}),
);
document.removeEventListener('message', handleIncomingMessage);
};
function handleIncomingMessage(e) {
const data = JSON.parse(e.data);
if (Array.isArray(data)) {
for (const message of data) {
try {
handleMessage(message);
} catch (err) {
handleError(err, message);
}
}
} else {
try {
handleMessage(data);
} catch (err) {
handleError(err, data);
}
}
}
document.addEventListener('message', handleIncomingMessage);
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment