├── .gitignore ├── src ├── index.web.js ├── index.js ├── console │ ├── index.js │ ├── count.js │ └── time.js ├── process.js ├── DOM │ ├── HTMLImageElement.js │ ├── HTMLVideoElement.js │ ├── Node.js │ ├── Element.js │ ├── HTMLCanvasElement.js │ └── Document.js ├── module.js ├── resize.js ├── performance.js └── window.js ├── .npmignore ├── demo ├── image.png └── zdog-and-tests.js ├── assets └── zdog-and-tests.gif ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | -------------------------------------------------------------------------------- /src/index.web.js: -------------------------------------------------------------------------------- 1 | // do nothing :) -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './module'; 2 | -------------------------------------------------------------------------------- /src/console/index.js: -------------------------------------------------------------------------------- 1 | import './count'; 2 | import './time'; 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /src/process.js: -------------------------------------------------------------------------------- 1 | process.browser = true; 2 | process.nextTick = setImmediate; 3 | -------------------------------------------------------------------------------- /demo/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flyskywhy/react-native-browser-polyfill/HEAD/demo/image.png -------------------------------------------------------------------------------- /src/DOM/HTMLImageElement.js: -------------------------------------------------------------------------------- 1 | export default from '@flyskywhy/react-native-gcanvas/packages/gcanvas/src/env/image'; 2 | -------------------------------------------------------------------------------- /assets/zdog-and-tests.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flyskywhy/react-native-browser-polyfill/HEAD/assets/zdog-and-tests.gif -------------------------------------------------------------------------------- /src/DOM/HTMLVideoElement.js: -------------------------------------------------------------------------------- 1 | import Element from './Element'; 2 | 3 | class HTMLVideoElement extends Element {} 4 | 5 | export default HTMLVideoElement; 6 | -------------------------------------------------------------------------------- /src/module.js: -------------------------------------------------------------------------------- 1 | import Document from './DOM/Document'; 2 | 3 | window.document = window.document || new Document(); 4 | import './window'; 5 | import './resize'; 6 | import './process'; 7 | import './console'; 8 | -------------------------------------------------------------------------------- /src/console/count.js: -------------------------------------------------------------------------------- 1 | if (!console.count) { 2 | const counts = {}; 3 | 4 | console.count = (label = '') => { 5 | if (!counts[label]) { 6 | counts[label] = 0; 7 | } 8 | counts[label]++; 9 | console.log(`${label}: ${counts[label]}`); 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/console/time.js: -------------------------------------------------------------------------------- 1 | const startTimes = {}; 2 | 3 | if (!console.time) { 4 | console.time = (label) => { 5 | startTimes[label] = window.performance.now(); 6 | }; 7 | } 8 | 9 | if (!console.timeEnd) { 10 | console.timeEnd = (label) => { 11 | const endTime = window.performance.now(); 12 | if (startTimes[label]) { 13 | const delta = endTime - startTimes[label]; 14 | console.log(`${label}: ${delta.toFixed(3)}ms`); 15 | delete startTimes[label]; 16 | } else { 17 | console.warn(`Warning: No such label '${label}' for console.timeEnd()`); 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/DOM/Node.js: -------------------------------------------------------------------------------- 1 | import {EventTarget} from 'event-target-shim'; 2 | 3 | const NODE_EVENTS = [ 4 | 'load', 5 | 'resize', 6 | 'mousedown', 7 | 'pointerdown', 8 | 'mousemove', 9 | 'pointermove', 10 | 'touchmove', 11 | 'mouseup', 12 | 'pointerup', 13 | 'ontouchend', 14 | ]; 15 | 16 | class Node extends EventTarget(...NODE_EVENTS) { 17 | constructor(nodeName) { 18 | super(); 19 | 20 | this.style = {}; 21 | this.className = { 22 | baseVal: '', 23 | }; 24 | this.nodeName = nodeName; 25 | } 26 | 27 | get ownerDocument() { 28 | return window.document; 29 | } 30 | 31 | appendChild() {} 32 | insertBefore() {} 33 | removeChild() {} 34 | } 35 | 36 | export default Node; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-present, 650 Industries, Inc. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/resize.js: -------------------------------------------------------------------------------- 1 | import {Dimensions} from 'react-native'; 2 | 3 | /* 4 | Window Resize Stub 5 | */ 6 | 7 | // devicePixelRatio is already set in @flyskywhy/react-native-gcanvas, 8 | // so it's strange and wrong scale to set again in js like zdog, so we 9 | // let devicePixelRatio be undefined here to let zdog set its own 10 | // pixelRatio as 1, ref to `node_modules/zdog/js/illustration.js` 11 | // window.devicePixelRatio = Dimensions.get('window').scale; 12 | 13 | window.innerWidth = Dimensions.get('window').width; 14 | window.clientWidth = window.innerWidth; 15 | window.innerHeight = Dimensions.get('window').height; 16 | window.clientHeight = window.innerHeight; 17 | window.screen = window.screen || {}; 18 | window.screen.orientation = 19 | window.screen.orientation || window.clientWidth < window.clientHeight 20 | ? 0 21 | : 90; 22 | if (!global.__RN_BROWSER_POLYFILL_RESIZE) { 23 | global.__RN_BROWSER_POLYFILL_RESIZE = true; 24 | Dimensions.addEventListener('change', () => { 25 | const {width, height, scale} = Dimensions.get('window'); 26 | // window.devicePixelRatio = scale; 27 | window.innerWidth = width; 28 | window.clientWidth = width; 29 | window.innerHeight = height; 30 | window.clientHeight = height; 31 | window.screen.orientation = width < height ? 0 : 90; 32 | window.document.dispatchEvent({type: 'resize'}); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/DOM/Element.js: -------------------------------------------------------------------------------- 1 | import Node from './Node'; 2 | 3 | class Element extends Node { 4 | constructor(tagName) { 5 | super(tagName.toUpperCase()); 6 | 7 | this.doc = { 8 | body: { 9 | innerHTML: '', 10 | }, 11 | }; 12 | } 13 | 14 | get tagName() { 15 | return this.nodeName; 16 | } 17 | 18 | setAttributeNS() {} 19 | 20 | getAttribute(attributeName) { 21 | return this[attributeName]; 22 | } 23 | 24 | setAttribute(name, value) { 25 | if (this.hasOwnProperty(name)) { 26 | this[name] = value; 27 | } 28 | } 29 | 30 | get clientWidth() { 31 | return this.innerWidth; 32 | } 33 | get clientHeight() { 34 | return this.innerHeight; 35 | } 36 | 37 | get offsetWidth() { 38 | return this.innerWidth; 39 | } 40 | get offsetHeight() { 41 | return this.innerHeight; 42 | } 43 | 44 | get innerWidth() { 45 | return window.innerWidth; 46 | } 47 | get innerHeight() { 48 | return window.innerHeight; 49 | } 50 | 51 | getBoundingClientRect() { 52 | return { 53 | left: 0, 54 | top: 0, 55 | right: window.innerWidth, 56 | bottom: window.innerHeight, 57 | x: 0, 58 | y: 0, 59 | width: window.innerWidth, 60 | height: window.innerHeight, 61 | }; 62 | } 63 | 64 | get ontouchstart() { 65 | return {}; 66 | } 67 | 68 | focus() {} 69 | } 70 | 71 | export default Element; 72 | -------------------------------------------------------------------------------- /src/DOM/HTMLCanvasElement.js: -------------------------------------------------------------------------------- 1 | import Element from './Element'; 2 | 3 | class HTMLCanvasElement extends Element { 4 | // Please use document.createElement('canvas') in Document.js which does 5 | // not use HTMLCanvasElement.js here if use createCanvasElements in windows.js 6 | getContext(contextType) { 7 | return { 8 | fillText: (text, x, y, maxWidth) => ({}), 9 | measureText: (text) => ({ 10 | width: (text || '').split('').length * 6, 11 | height: 24, 12 | }), 13 | fillRect: () => ({}), 14 | drawImage: () => ({}), 15 | getImageData: () => ({data: new Uint8ClampedArray([255, 0, 0, 0])}), 16 | putImageData: () => ({}), 17 | createImageData: () => ({}), 18 | 19 | // even OpenGL ES not support getContextAttributes as described in 20 | // https://github.com/flyskywhy/react-native-gcanvas/blob/2.3.33/core/src/gcanvas/GWebglContext.cpp#L1251 21 | // and functions above are for 'canvas' below are for 'webgl', 22 | // still define getContextAttributes here for running well with 23 | // https://github.com/flyskywhy/snakeRN/tree/v3.1.1 24 | getContextAttributes: () => ({ 25 | stencil: true, 26 | }), 27 | 28 | getExtension: () => ({ 29 | loseContext: () => {}, 30 | }), 31 | }; 32 | } 33 | 34 | toDataURL() { 35 | return ''; 36 | } 37 | } 38 | 39 | export default HTMLCanvasElement; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flyskywhy/react-native-browser-polyfill", 3 | "version": "2.0.2", 4 | "sideEffects": false, 5 | "description": "Browser polyfill with native canvas 2d 3d for making React Native compatible with web libs like zdog (and pixi.js, three.js, phaser.js to be validated)", 6 | "homepage": "https://github.com/flyskywhy/react-native-browser-polyfill#readme", 7 | "bugs": { 8 | "url": "https://github.com/flyskywhy/react-native-browser-polyfill/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/flyskywhy/react-native-browser-polyfill.git" 13 | }, 14 | "keywords": [ 15 | "2d", 16 | "3d", 17 | "browser", 18 | "document", 19 | "dogz", 20 | "dom", 21 | "offscreen-canvas", 22 | "polyfill", 23 | "react", 24 | "react-native", 25 | "shim", 26 | "web", 27 | "window", 28 | "zdog" 29 | ], 30 | "private": false, 31 | "author": { 32 | "email": "bacon@expo.io", 33 | "name": "Evan Bacon" 34 | }, 35 | "contributors": [ 36 | { 37 | "name": "Li Zheng", 38 | "email": "flyskywhy@gmail.com", 39 | "url": "https://github.com/flyskywhy" 40 | } 41 | ], 42 | "license": "MIT", 43 | "readmeFilename": "README.md", 44 | "main": "src/index", 45 | "scripts": { 46 | "test": "echo \"Error: no test specified\" && exit 1" 47 | }, 48 | "peerDependencies": { 49 | "@flyskywhy/react-native-gcanvas": "*", 50 | "react": "*", 51 | "react-native": "*" 52 | }, 53 | "dependencies": { 54 | "@canvas/image-data": "^1.0.0", 55 | "atob": "2.1.2", 56 | "btoa": "1.2.1", 57 | "buffer": "^5.6.0", 58 | "fbjs": "^1.0.0", 59 | "text-encoding": "^0.7.0", 60 | "xmldom-qsa": "^1.0.3" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/DOM/Document.js: -------------------------------------------------------------------------------- 1 | import Element from './Element'; 2 | import HTMLVideoElement from './HTMLVideoElement'; 3 | import HTMLImageElement from './HTMLImageElement'; 4 | import HTMLCanvasElement from './HTMLCanvasElement'; 5 | 6 | class Document extends Element { 7 | constructor() { 8 | super('#document'); 9 | this.body = new Element('BODY'); 10 | this.documentElement = new Element('HTML'); 11 | this.readyState = 'complete'; 12 | } 13 | 14 | createElement(tagName) { 15 | switch ((tagName || '').toLowerCase()) { 16 | case 'video': 17 | return new HTMLVideoElement(tagName); 18 | case 'img': 19 | return new HTMLImageElement(tagName); 20 | case 'canvas': 21 | const createCanvasElementsObjKeys = Object.keys(global.createCanvasElementsObj); 22 | if (createCanvasElements.length === 0 && createCanvasElementsObjKeys.length === 0) { 23 | // the reason of "begining of your APP render()" here, is that most document.createElement('canvas') 24 | // (as offscreen canvas) is invoked in another onscreen canvas's onCanvasCreate() and onCanvasResize() , 25 | // so if not "begining of your APP render()" or just not before onscreen canvas's , 26 | // will cause `TypeError: undefined is not an object (evaluating 'canvas.width = ')` when onscreen canvas 27 | // set offscreen canvas.width in onscreen canvas' onCanvasCreate() 28 | console.warn('Need one for each createElement at the begining of your APP render()!'); 29 | return new HTMLCanvasElement(tagName); 30 | return; 31 | } else if (createCanvasElementsObjKeys.length) { 32 | if (createCanvasObjCurrent === undefined) { 33 | createCanvasObjCurrent = 0; 34 | } else { 35 | createCanvasObjCurrent = (createCanvasObjCurrent + 1) % createCanvasElementsObjKeys.length; 36 | } 37 | 38 | return createCanvasElementsObj[createCanvasElementsObjKeys[createCanvasObjCurrent]]; 39 | } else { 40 | if (createCanvasCurrent === undefined) { 41 | createCanvasCurrent = 0; 42 | } else { 43 | createCanvasCurrent = (createCanvasCurrent + 1) % createCanvasElements.length; 44 | } 45 | 46 | return createCanvasElements[createCanvasCurrent]; 47 | } 48 | case 'iframe': 49 | // Return nothing to keep firebase working. 50 | return null; 51 | default: 52 | return new Element(tagName); 53 | } 54 | } 55 | 56 | createElementNS(tagName) { 57 | return this.createElement(tagName); 58 | } 59 | 60 | getElementById(id) { 61 | return new Element('div'); 62 | } 63 | } 64 | 65 | export default Document; 66 | -------------------------------------------------------------------------------- /src/performance.js: -------------------------------------------------------------------------------- 1 | if (!global.nativePerformanceNow) { 2 | global.nativePerformanceNow = 3 | global.nativePerformanceNow || 4 | global.performanceNow || 5 | require('fbjs/lib/performanceNow'); 6 | global.performanceNow = global.performanceNow || global.nativePerformanceNow; 7 | } 8 | 9 | // Below duplicate `react-native/Libraries/Core/setUpPerformance.js`, so comment below 10 | 11 | // if (!global.performance) { 12 | // global.performance = {}; 13 | // } 14 | 15 | // if (!global.performance.now) { 16 | // global.performance.now = function () { 17 | // // TODO: Bacon: Return DOMHighResTimeStamp 18 | // const performanceNow = global.nativePerformanceNow || Date.now; 19 | // return performanceNow(); 20 | // }; 21 | // } 22 | 23 | if (!global.performance.timeOrigin) { 24 | global.performance.timeOrigin = -1; 25 | } 26 | 27 | if (!global.performance.timing) { 28 | global.performance.timing = { 29 | connectEnd: -1, 30 | connectStart: -1, 31 | domComplete: -1, 32 | domContentLoadedEventEnd: -1, 33 | domContentLoadedEventStart: -1, 34 | domInteractive: -1, 35 | domLoading: -1, 36 | domainLookupEnd: -1, 37 | domainLookupStart: -1, 38 | fetchStart: -1, 39 | loadEventEnd: -1, 40 | loadEventStart: -1, 41 | navigationStart: -1, 42 | redirectEnd: -1, 43 | redirectStart: -1, 44 | requestStart: -1, 45 | responseEnd: -1, 46 | responseStart: -1, 47 | secureConnectionStart: -1, 48 | unloadEventEnd: -1, 49 | unloadEventStart: -1, 50 | }; 51 | } 52 | 53 | if (!global.performance.toJSON) { 54 | global.performance.toJSON = function () { 55 | return { 56 | timing: this.timing, 57 | navigation: this.navigation, 58 | timeOrigin: this.timeOrigin, 59 | }; 60 | }; 61 | } 62 | 63 | if (!global.performance.navigation) { 64 | global.performance.navigation = { 65 | redirectCount: -1, 66 | type: -1, 67 | }; 68 | } 69 | 70 | if (!global.performance.memory) { 71 | global.performance.memory = { 72 | jsHeapSizeLimit: -1, 73 | totalJSHeapSize: -1, 74 | usedJSHeapSize: -1, 75 | }; 76 | } 77 | 78 | // if (!global.performance.measure) { 79 | // global.performance.measure = function () { 80 | // console.warn('window.performance.measure is not implemented'); 81 | // }; 82 | // } 83 | 84 | // if (!global.performance.mark) { 85 | // global.performance.mark = function () { 86 | // console.warn('window.performance.mark is not implemented'); 87 | // }; 88 | // } 89 | 90 | // if (!global.performance.clearMeasures) { 91 | // global.performance.clearMeasures = function () { 92 | // console.warn('window.performance.clearMeasures is not implemented'); 93 | // }; 94 | // } 95 | 96 | // this will stuck the APP, so comment this and related above 97 | // if (!global.performance.clearMarks) { 98 | // global.performance.clearMarks = function () { 99 | // console.warn('window.performance.clearMarks is not implemented'); 100 | // }; 101 | // } 102 | 103 | if (!global.performance.clearResourceTimings) { 104 | global.performance.clearResourceTimings = function () { 105 | console.warn('window.performance.clearResourceTimings is not implemented'); 106 | }; 107 | } 108 | 109 | if (!global.performance.setResourceTimingBufferSize) { 110 | global.performance.setResourceTimingBufferSize = function () { 111 | console.warn( 112 | 'window.performance.setResourceTimingBufferSize is not implemented', 113 | ); 114 | }; 115 | } 116 | 117 | if (!global.performance.getEntriesByType) { 118 | global.performance.getEntriesByType = function () { 119 | console.warn('window.performance.getEntriesByType is not implemented'); 120 | }; 121 | } 122 | 123 | if (!global.performance.getEntriesByName) { 124 | global.performance.getEntriesByName = function () { 125 | console.warn('window.performance.getEntriesByName is not implemented'); 126 | }; 127 | } 128 | 129 | if (!global.performance.getEntries) { 130 | global.performance.getEntries = function () { 131 | console.warn('window.performance.getEntries is not implemented'); 132 | }; 133 | } 134 | 135 | export default window.performance; 136 | -------------------------------------------------------------------------------- /src/window.js: -------------------------------------------------------------------------------- 1 | import {Buffer} from 'buffer'; 2 | import {TextDecoder, TextEncoder} from 'text-encoding'; 3 | import atob from 'atob/node-atob.js'; 4 | import btoa from 'btoa'; 5 | import Element from './DOM/Element'; 6 | import Document from './DOM/Document'; 7 | 8 | import './performance'; 9 | 10 | import HTMLImageElement from './DOM/HTMLImageElement'; 11 | import HTMLCanvasElement from './DOM/HTMLCanvasElement'; 12 | import HTMLVideoElement from './DOM/HTMLVideoElement'; 13 | import CanvasRenderingContext2D from '@flyskywhy/react-native-gcanvas/packages/gcanvas/src/context/2d/RenderingContext'; 14 | import WebGLRenderingContext from '@flyskywhy/react-native-gcanvas/packages/gcanvas/src/context/webgl/RenderingContext'; 15 | import ImageData from '@canvas/image-data/index'; 16 | 17 | global.Document = global.Document || Document; 18 | global.Element = global.Element || Element; 19 | global.HTMLImageElement = global.HTMLImageElement || HTMLImageElement; 20 | global.Image = global.Image || HTMLImageElement; 21 | global.ImageBitmap = global.ImageBitmap || HTMLImageElement; 22 | global.HTMLVideoElement = global.HTMLVideoElement || HTMLVideoElement; 23 | global.Video = global.Video || HTMLVideoElement; 24 | global.HTMLCanvasElement = global.HTMLCanvasElement || HTMLCanvasElement; 25 | global.Canvas = global.Canvas || HTMLCanvasElement; 26 | global.CanvasRenderingContext2D = 27 | global.CanvasRenderingContext2D || CanvasRenderingContext2D; 28 | global.WebGLRenderingContext = 29 | global.WebGLRenderingContext || WebGLRenderingContext; 30 | global.ImageData = ImageData; 31 | 32 | // createCanvasElements and createCanvasCurrent are not members of standard global, they are 33 | // used for document.createElement('canvas') (as offscreen canvas) works with relevant offscreenCanvas={true} component: 34 | // this.setState({hasOc1: true})} // it's better to setState some as describe in https://github.com/flyskywhy/react-native-gcanvas/blob/master/README.MD 45 | // devicePixelRatio={1} // should not 1 < devicePixelRatio < 2 as float to avoid pixel offset flaw when GetImageData with PixelsSampler in @flyskywhy/react-native-gcanvas/core/src/support/GLUtil.cpp 46 | // isGestureResponsible={false} // who will need gesture with offscreen canvas? 47 | // /> 48 | global.createCanvasElements = []; 49 | global.createCanvasCurrent = undefined; 50 | // ios release (RN0.71.6 JSC) createCanvasElements.push(canvas) in a class but still 51 | // get [] means createCanvasElements.length is 0 in another class, so have to 52 | // use createCanvasElementsObj below, and reserve createCanvasElements for compatible 53 | global.createCanvasElementsObj = {}; 54 | global.createCanvasObjCurrent = undefined; 55 | 56 | window.scrollTo = window.scrollTo || (() => ({})); 57 | 58 | window.addEventListener = (eventName, listener) => { 59 | window.document.addEventListener(eventName, listener); 60 | 61 | if (eventName.toLowerCase() === 'load') { 62 | setTimeout(() => { 63 | window.document.dispatchEvent({type: 'load'}); 64 | }, 1); 65 | } 66 | }; 67 | 68 | window.removeEventListener = (eventName, listener) => 69 | window.document.removeEventListener(eventName, listener); 70 | 71 | window.dispatchEvent = (event) => window.document.dispatchEvent(event); 72 | 73 | window.DOMParser = window.DOMParser || require('xmldom-qsa').DOMParser; 74 | window.atob = atob; 75 | window.btoa = btoa; 76 | global.Buffer = Buffer; 77 | global.TextDecoder = global.TextDecoder || TextDecoder; 78 | global.TextEncoder = global.TextEncoder || TextEncoder; 79 | 80 | const agent = 'chrome'; 81 | global.userAgent = global.userAgent || agent; 82 | global.navigator.userAgent = global.navigator.userAgent || agent; 83 | global.navigator.product = 'ReactNative'; 84 | global.navigator.platform = global.navigator.platform || []; 85 | global.navigator.appVersion = global.navigator.appVersion || 'OS10'; 86 | global.navigator.maxTouchPoints = global.navigator.maxTouchPoints || 5; 87 | global.navigator.standalone = global.navigator.hasOwnProperty('standalone') 88 | ? global.navigator.standalone 89 | : true; 90 | 91 | window.chrome = window.chrome || { 92 | extension: {}, 93 | }; 94 | 95 | // https://www.w3schools.com/js/js_window_location.asp 96 | window.location = window.location || { 97 | href: '', // window.location.href returns the href (URL) of the current page 98 | hostname: '', //window.location.hostname returns the domain name of the web host 99 | pathname: '', //window.location.pathname returns the path and filename of the current page 100 | protocol: 'https', //window.location.protocol returns the web protocol used (http: or https:) 101 | assign: null, //window.location.assign loads a new document 102 | }; 103 | 104 | if (global.document) { 105 | global.document.readyState = 'complete'; 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @flyskywhy/react-native-browser-polyfill 2 | 3 | Browser polyfill with native canvas 2d 3d for React Native, can directly use canvas based html frameworks without code modified, e.g. [zdog](https://github.com/metafizzy/zdog). 4 | 5 | `@flyskywhy/react-native-browser-polyfill` is forked from [@expo/browser-polyfill](https://github.com/expo/browser-polyfill), then use `@flyskywhy/react-native-gcanvas` instead of `expo-2d-context`, use `event-target-shim` instead of `fbemitter`, and fix some bugs. 6 | 7 | ### Installation 8 | 9 | ```bash 10 | npm install @flyskywhy/react-native-browser-polyfill @flyskywhy/react-native-gcanvas 11 | ``` 12 | 13 | And follow [the steps to install @flyskywhy/react-native-gcanvas](https://github.com/flyskywhy/react-native-gcanvas#react-native). 14 | 15 | ### Usage 16 | 17 | Import the library just into your project root `/index.js` , `/index.android.js` , `/index.ios.js` or `/index.native.js`. 18 | 19 | If you don't want import it in your project root, you can also import the library into any JavaScript file where you want use it. But if `inlineRequires` is true in your `metro.config.js`, you will get `ERROR ReferenceError: Can't find variable: document` or `ERROR ReferenceError: Property 'document' doesn't exist, js engine: hermes`, then you should change `inlineRequires` to false or import the library into your project root e.g. `/index.android.js`. 20 | 21 | ```js 22 | import '@flyskywhy/react-native-browser-polyfill'; 23 | ``` 24 | 25 | If canvas 2d or 3d needed, ref to README.md of [@flyskywhy/react-native-gcanvas](https://github.com/flyskywhy/react-native-gcanvas), or just ref to [ZdogAndTests.js](https://github.com/flyskywhy/GCanvasRNExamples/blob/master/app/components/ZdogAndTests.js). 26 | 27 | ## Example 28 | ### zdog 29 | No need modify any code of framework [zdog](https://github.com/metafizzy/zdog) itself. 30 | 31 | Only modify one line code of app demo [Made with Zdog CodePen Collection](https://codepen.io/collection/DzdGMe/), e.g. just modify `.zdog-canvas` in `JS` of [https://codepen.io/clarke-nicol/pen/OezRdM](https://codepen.io/clarke-nicol/pen/OezRdM) into `this.canvas` in this GCanvasRNExamples APP commit [react -> react-native: `Zdog and Tests` step3 Zdog works well](https://github.com/flyskywhy/GCanvasRNExamples/commit/7855e91). 32 | 33 | Here is the result of [ZdogAndTests.js](https://github.com/flyskywhy/GCanvasRNExamples/blob/master/app/components/ZdogAndTests.js), you can directly discover that the `render` and `mousemove` is same with the original html version [https://codepen.io/clarke-nicol/pen/OezRdM](https://codepen.io/clarke-nicol/pen/OezRdM). 34 | 35 | 36 | 37 | ## Implements 38 | 39 | ## DOM 40 | 41 | DOM is provided with very low support, these are used for libs like pixi.js that validate type. 42 | 43 | ```js 44 | class Node 45 | class Element 46 | class Document 47 | class HTMLImageElement 48 | class Image 49 | class ImageBitmap 50 | class HTMLVideoElement 51 | class Video 52 | class HTMLCanvasElement 53 | class Canvas 54 | ``` 55 | 56 | ### Image, HTMLImageElement, ImageBitmap 57 | 58 | Image can load from `https://somewhere.com/some.png` or from require('some.png') on Android, iOS and Web, ref to the [ZdogAndTests.js](https://github.com/flyskywhy/GCanvasRNExamples/blob/master/app/components/ZdogAndTests.js) or [nonDeclarative.js](https://github.com/flyskywhy/GCanvasRNExamples/blob/master/src/nonDeclarative.js). 59 | 60 | ```js 61 | const image = document.createElement('img'); 62 | const image = new global.HTMLImageElement(); 63 | const image = new Image(); 64 | 65 | image.onload = () => console.log('Can recieve event load by onload'); 66 | image.addEventListener('load', () => console.log('Also can recieve event load by addEventListener'); 67 | ``` 68 | 69 | ### Canvas, HTMLCanvasElement 70 | [Example As Usage of @flyskywhy/react-native-gcanvas](https://github.com/flyskywhy/react-native-gcanvas#example-as-usage) 71 | 72 | `document.createElement('canvas')` (as offscreen canvas) usage also is described in `src/window.js`. 73 | 74 | ### window as global 75 | 76 | ```js 77 | addEventListener; 78 | removeEventListener; 79 | dispatchEvent; 80 | Buffer; 81 | TextDecoder; 82 | TextEncoder; 83 | document; 84 | Document; 85 | Element; 86 | Image; 87 | HTMLImageElement; 88 | ImageBitmap; 89 | CanvasRenderingContext2D; 90 | WebGLRenderingContext; 91 | // window.devicePixelRatio; // undefined as described in `src\resize.js` 92 | window.screen.orientation; 93 | userAgent; 94 | location; 95 | ``` 96 | 97 | ### Document 98 | 99 | ```js 100 | document.createElement; 101 | document.createElementNS; 102 | ``` 103 | 104 | ### Element 105 | 106 | #### All sizes return the window size: 107 | 108 | ```js 109 | element.clientWidth; 110 | element.clientHeight; 111 | element.innerWidth; 112 | element.innerHeight; 113 | element.offsetWidth; 114 | element.offsetHeight; 115 | element.getBoundingClientRect; 116 | element.getAttribute; 117 | element.setAttribute; 118 | ``` 119 | 120 | #### Empty attributes that prevent libraries from crashing 121 | 122 | ```js 123 | element.tagName; 124 | element.setAttributeNS; 125 | element.focus; 126 | ``` 127 | 128 | ### Node 129 | 130 | ```js 131 | node.ownerDocument; 132 | node.className; 133 | node.addEventListener; 134 | node.removeEventListener; 135 | node.dispatchEvent; 136 | node.appendChild; 137 | node.insertBefore; 138 | node.removeChild; 139 | ``` 140 | 141 | # External Libraries 142 | 143 | Some external node.js polyfills are added as well. 144 | 145 | ## [text-encoding](https://github.com/inexorabletash/text-encoding) 146 | 147 | ``` 148 | global.TextEncoder 149 | global.TextDecoder 150 | ``` 151 | 152 | ## [xmldom-qsa](https://github.com/zeligzhou/xmldom-qsa) 153 | 154 | ``` 155 | window.DOMParser 156 | ``` 157 | -------------------------------------------------------------------------------- /demo/zdog-and-tests.js: -------------------------------------------------------------------------------- 1 | // ref to https://github.com/flyskywhy/GCanvasRNExamples/blob/master/app/components/ZdogAndTests.js 2 | 3 | import React, {Component} from 'react'; 4 | import { 5 | Platform, 6 | StyleSheet, 7 | Text, 8 | TouchableHighlight, 9 | View, 10 | } from 'react-native'; 11 | 12 | // also can just move this line into your root `/index.js` , `/index.android.js` , `/index.ios.js` or `/index.native.js`. 13 | import '@flyskywhy/react-native-browser-polyfill'; 14 | 15 | import {GCanvasView} from '@flyskywhy/react-native-gcanvas'; 16 | import {Asset} from 'expo-asset'; 17 | import * as FileSystem from 'expo-file-system'; 18 | import Zdog from 'zdog'; 19 | 20 | const tests = { 21 | timer: () => { 22 | const tag = 'test-timer'; 23 | console.time(tag); 24 | console.timeEnd(tag); 25 | console.count(tag); 26 | }, 27 | userAgent: () => { 28 | console.log('userAgent: ', global.userAgent); 29 | }, 30 | process: () => { 31 | console.log('process: ', global.process); 32 | }, 33 | navigator: () => { 34 | console.log('navigator: ', global.navigator); 35 | }, 36 | performance: () => { 37 | console.log('performance: ', Object.keys(global.performance)); 38 | }, 39 | window: () => { 40 | console.log('location: ', window.location); 41 | }, 42 | base64: async () => { 43 | const asset = Asset.fromModule(require('./image.png')); 44 | await asset.downloadAsync(); 45 | if (Platform.OS === 'web') { 46 | const {localUri, width, height} = asset; 47 | console.log('B64: Loaded Image', {localUri, width, height}); 48 | return; 49 | } else { 50 | console.log('B64: ASSET:', asset.localUri); 51 | } 52 | const data = ( 53 | await FileSystem.readAsStringAsync(asset.localUri, { 54 | encoding: FileSystem.EncodingType.Base64, 55 | }) 56 | ).trim(); 57 | 58 | const pngPrefix = 'data:image/png;base64,'; 59 | // console.log('B64: DATA: ', pngPrefix + data); 60 | const image = new global.HTMLImageElement(); 61 | image.addEventListener('error', () => { 62 | console.log('B64: Error Loading Image'); 63 | }); 64 | image.onload = () => { 65 | const {src, width, height} = image; 66 | console.log('B64: Loaded Image', {src, width, height}); 67 | }; 68 | image.src = pngPrefix + data; 69 | }, 70 | correctElementsCreated: () => { 71 | const { 72 | HTMLImageElement, 73 | ImageBitmap, 74 | HTMLVideoElement, 75 | HTMLCanvasElement, 76 | } = global; 77 | const types = [ 78 | {type: 'img', inst: HTMLImageElement}, 79 | {type: 'video', inst: HTMLVideoElement}, 80 | {type: 'canvas', inst: HTMLCanvasElement}, 81 | {type: 'img', inst: ImageBitmap}, 82 | ]; 83 | types.forEach((item) => { 84 | const element = document.createElement(item.type); 85 | console.log( 86 | 'type', 87 | item.type, 88 | element instanceof item.inst, 89 | element instanceof Number, 90 | ); 91 | }); 92 | }, 93 | elements: () => { 94 | const { 95 | HTMLImageElement, 96 | Image, 97 | ImageBitmap, 98 | HTMLVideoElement, 99 | Video, 100 | HTMLCanvasElement, 101 | Canvas, 102 | } = global; 103 | const elements = { 104 | HTMLImageElement, 105 | Image, 106 | ImageBitmap, 107 | HTMLVideoElement, 108 | Video, 109 | HTMLCanvasElement, 110 | Canvas, 111 | }; 112 | console.warn( 113 | 'Elements: ', 114 | Object.keys(elements).map((key) => ({[key]: !!elements[key]})), 115 | ); 116 | }, 117 | loadImage: () => { 118 | const image = document.createElement('img'); 119 | image.addEventListener('error', () => { 120 | console.log('Error Loading Image'); 121 | }); 122 | image.onload = () => { 123 | const {src, width, height} = image; 124 | console.log('Loaded Image', {src, width, height}); 125 | }; 126 | image.src = 'https://avatars0.githubusercontent.com/u/9664363?s=40&v=4'; 127 | }, 128 | textDecoder: () => { 129 | console.log('TextDecoder: ', !!global.TextDecoder); 130 | const utfLabel = 'utf-8'; 131 | const options = {fatal: true}; 132 | const decoder = new TextDecoder(utfLabel, options); 133 | 134 | let data = Buffer.from('Hello'); 135 | let buffer = ''; 136 | buffer += decoder.decode(data, {stream: true}); 137 | console.log('TextDecoder buffer', buffer, ' from: ', data); 138 | }, 139 | context2d: () => { 140 | const canvas = document.createElement('canvas'); 141 | const context = canvas.getContext('2d'); 142 | console.log(context); 143 | }, 144 | contextwebgl: () => { 145 | const canvas = document.createElement('canvas'); 146 | const webgl = canvas.getContext('webgl'); 147 | console.log(webgl); 148 | }, 149 | domParser: () => { 150 | console.log('window.DOMParser: ', !!window.DOMParser); 151 | const parser = new window.DOMParser(); 152 | 153 | const html = ` 154 | 155 | 156 | 157 |
158 | some text content 159 |

Charlie Cheever

160 |

is my dad.

161 | 162 |
163 | 164 | 165 | `; 166 | const dom = parser.parseFromString(html, 'text/html'); 167 | const container = dom.getElementById('container-id'); 168 | 169 | console.log('_nsMap', typeof container._nsMap); 170 | console.log('attributes', typeof container.attributes); 171 | console.log('childNodes', typeof container.childNodes); 172 | console.log('ownerDocument', typeof container.ownerDocument); 173 | console.log('nodeName', container.nodeName); // div on native; DIV on Web 174 | console.log('tagName', container.tagName); // div on native; DIV on Web 175 | console.log('namespaceURI', container.namespaceURI); 176 | console.log('localName', container.localName); // div on native; div on Web 177 | console.log('parentNode', typeof container.parentNode); 178 | console.log('previousSibling', typeof container.previousSibling); 179 | console.log('lineNumber', container.lineNumber); // 5 on native; undefined on Web 180 | console.log('nextSibling', typeof container.nextSibling); 181 | console.log('columnNumber', container.columnNumber); // 9 on native; undefined on Web 182 | console.log('firstChild', typeof container.firstChild); 183 | console.log('lastChild', typeof container.lastChild); 184 | // Object.keys(container) on Web is [] , so use above instead 185 | // Object.keys(container).forEach((key) => { 186 | // const obj = container[key]; 187 | // console.log(`DOMParser: container.${key}: ${obj}`); 188 | // }); 189 | }, 190 | }; 191 | 192 | export default class App extends Component { 193 | constructor(props) { 194 | super(props); 195 | this.canvas = null; 196 | 197 | // only useful on Android, because it's always true on iOS 198 | this.isGReactTextureViewReady = true; 199 | } 200 | 201 | componentDidMount() { 202 | window.addEventListener('resize', this.onLayout); 203 | } 204 | 205 | componentWillUnmount() { 206 | window.removeEventListener('resize', this.onLayout); 207 | } 208 | 209 | onLayout = () => { 210 | console.log( 211 | 'Update Layout:', 212 | window.innerWidth, 213 | window.innerHeight, 214 | window.screen.orientation, 215 | ); 216 | }; 217 | 218 | runTests = () => { 219 | Object.keys(tests).forEach((key) => { 220 | try { 221 | console.log('Run Test: ', key); 222 | tests[key](); 223 | } catch (error) { 224 | console.error(`Error running ${key} test: `, error); 225 | } 226 | }); 227 | }; 228 | 229 | initCanvas = (canvas) => { 230 | if (this.canvas) { 231 | return; 232 | } 233 | 234 | this.canvas = canvas; 235 | if (Platform.OS === 'web') { 236 | // canvas.width not equal canvas.clientWidth, so have to assign again 237 | this.canvas.width = this.canvas.clientWidth; 238 | this.canvas.height = this.canvas.clientHeight; 239 | } 240 | }; 241 | 242 | zdog = () => { 243 | if (this.canvas) { 244 | let illo = new Zdog.Illustration({ 245 | element: this.canvas, 246 | dragRotate: true, 247 | onDragStart: function () { 248 | isSpinning = false; 249 | }, 250 | onDragEnd: function () { 251 | isSpinning = false; 252 | }, 253 | zoom: 30, 254 | }); 255 | 256 | var sponge = '#F2CDAC'; 257 | var icing = '#BF4158'; 258 | var jam = '#F25D50'; 259 | 260 | let cake = new Zdog.Anchor({ 261 | addTo: illo, 262 | visible: true, 263 | rotate: {x: -Zdog.TAU / 8, y: -Zdog.TAU / 18}, 264 | }); 265 | 266 | let cakeTop = new Zdog.Shape({ 267 | addTo: cake, 268 | path: [ 269 | {x: 0, z: -3}, 270 | {x: 2.2, z: -2.2}, 271 | {x: 3, z: 0}, 272 | {x: 2.2, z: 2.2}, 273 | {x: 0, z: 3}, 274 | {x: 0, z: 0}, 275 | {x: -2.2, z: 2.2}, 276 | {x: -3, z: 0}, 277 | {x: -2.2, z: -2.2}, 278 | ], 279 | stroke: 10 / illo.zoom, 280 | fill: true, 281 | color: icing, 282 | translate: {y: -0.05}, 283 | }); 284 | 285 | let cakeBottom = cakeTop.copy({ 286 | translate: {y: 2}, 287 | color: sponge, 288 | stroke: 4 / illo.zoom, 289 | }); 290 | 291 | var sideGroup = new Zdog.Group({ 292 | addTo: cake, 293 | rotate: {y: -Zdog.TAU / 18}, 294 | }); 295 | 296 | let insideLeft = new Zdog.Shape({ 297 | addTo: cake, 298 | path: [ 299 | {x: -2.2, y: 0.5, z: 2.2}, 300 | {x: 0, y: 0.5, z: 0}, 301 | {x: 0, y: 2, z: 0}, 302 | {x: -2.2, y: 2, z: 2.2}, 303 | ], 304 | stroke: 1 / illo.zoom, 305 | fill: true, 306 | color: sponge, 307 | backface: false, 308 | //color: 'red', 309 | }); 310 | 311 | let insideLeftJam = new Zdog.Shape({ 312 | addTo: cake, 313 | path: [ 314 | {x: -0.25, y: 0.9, z: 0.5}, 315 | {x: -2.2, y: 0.9, z: 2.35}, 316 | ], 317 | stroke: 10 / illo.zoom, 318 | color: jam, 319 | }); 320 | 321 | let insideLeftCream = insideLeftJam.copy({ 322 | addTo: cake, 323 | color: 'white', 324 | translate: {y: 0.2}, 325 | }); 326 | 327 | let insideRight = new Zdog.Shape({ 328 | addTo: cake, 329 | path: [ 330 | {x: 0, y: 1, z: 2.95}, 331 | {x: 0, y: 1, z: 0}, 332 | {x: 0, y: 2, z: 0}, 333 | {x: 0, y: 2, z: 2.95}, 334 | ], 335 | stroke: 1 / illo.zoom, 336 | fill: true, 337 | color: sponge, 338 | backface: false, 339 | //color: 'red', 340 | }); 341 | 342 | let insideRightJam = new Zdog.Shape({ 343 | addTo: cake, 344 | path: [ 345 | {x: -0.25, y: 0.9, z: 0.5}, 346 | {x: 0, y: 0.9, z: 2.95}, 347 | ], 348 | stroke: 10 / illo.zoom, 349 | color: jam, 350 | }); 351 | 352 | let insideRightCream = insideRightJam.copy({ 353 | addTo: cake, 354 | color: 'white', 355 | translate: {y: 0.2}, 356 | }); 357 | 358 | let side = new Zdog.Rect({ 359 | addTo: sideGroup, 360 | width: 2.4, 361 | height: 2, 362 | stroke: 1 / illo.zoom, 363 | translate: {x: 0.15, y: 1, z: 2.85}, 364 | color: sponge, 365 | fill: true, 366 | }); 367 | 368 | let icingLeft = new Zdog.Ellipse({ 369 | addTo: side, 370 | diameter: 1.2, 371 | quarters: 2, 372 | stroke: 4 / illo.zoom, 373 | color: icing, 374 | fill: true, 375 | rotate: {z: Zdog.TAU / 4}, 376 | translate: {x: 0.6, y: -1.02}, 377 | }); 378 | 379 | let icingRight = icingLeft.copy({ 380 | translate: {x: -0.6, y: -1.02}, 381 | }); 382 | 383 | let jamFilling = new Zdog.Shape({ 384 | addTo: side, 385 | path: [ 386 | {x: -1.15, y: -0.1, z: 0.02}, 387 | {x: 1.2, y: -0.1, z: 0}, 388 | ], 389 | stroke: 10 / illo.zoom, 390 | fill: true, 391 | color: jam, 392 | }); 393 | 394 | let cream = jamFilling.copy({ 395 | color: 'white', 396 | translate: {y: 0.2}, 397 | }); 398 | 399 | sideGroup.copyGraph({ 400 | rotate: {y: Zdog.TAU / 2.25}, 401 | }); 402 | 403 | sideGroup.copyGraph({ 404 | translate: {x: 0.1, z: -0.3}, 405 | rotate: {y: Zdog.TAU / 3.27}, 406 | }); 407 | 408 | sideGroup.copyGraph({ 409 | translate: {x: 0, z: 0}, 410 | rotate: {y: -Zdog.TAU / 3.27}, 411 | }); 412 | sideGroup.copyGraph({ 413 | translate: {x: -0.025, z: -0.05}, 414 | rotate: {y: -Zdog.TAU / 1.24}, 415 | }); 416 | sideGroup.copyGraph({ 417 | translate: {x: 0.3, z: 0.1}, 418 | rotate: {y: Zdog.TAU / 1.8}, 419 | }); 420 | sideGroup.copyGraph({ 421 | translate: {x: -0.1, z: 0.3}, 422 | rotate: {y: Zdog.TAU / 1.24}, 423 | }); 424 | 425 | let ticker = 0; 426 | let cycleCount = 200; 427 | let isSpinning = true; 428 | 429 | function animate() { 430 | let progress = ticker / cycleCount; 431 | let tween = Zdog.easeInOut(progress % 1, 3); 432 | 433 | if (isSpinning) { 434 | illo.rotate.y = tween * -Zdog.TAU; 435 | } 436 | 437 | ticker++; 438 | 439 | illo.updateRenderGraph(); 440 | requestAnimationFrame(animate); 441 | } 442 | animate(); 443 | } 444 | }; 445 | 446 | render() { 447 | return ( 448 | 449 | 450 | 451 | Click me to render zdog and "mousemove" it 452 | 453 | 454 | {Platform.OS === 'web' ? ( 455 | 464 | ) : ( 465 | (this.isGReactTextureViewReady = value)} 468 | style={styles.gcanvas} 469 | /> 470 | )} 471 | 472 | 473 | Click me runTests react-native-browser-polyfill 474 | 475 | 476 | 477 | ); 478 | } 479 | } 480 | 481 | const styles = StyleSheet.create({ 482 | container: { 483 | flex: 1, 484 | justifyContent: 'center', 485 | alignItems: 'center', 486 | backgroundColor: '#F5FCFF', 487 | }, 488 | gcanvas: { 489 | width: 200, 490 | height: 300, 491 | // backgroundColor: '#FF000030', // TextureView doesn't support displaying a background drawable since Android API 24 492 | }, 493 | welcome: { 494 | fontSize: 20, 495 | textAlign: 'center', 496 | marginVertical: 20, 497 | }, 498 | }); 499 | --------------------------------------------------------------------------------