初步增加, 扫码登录成功
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
/**
|
||||
* jsdom 环境补丁(skill 路径 B 第 4 步,最小化:只补 kepler 实际读取项)。
|
||||
* 数据源:scripts/capture-kepler-env.mjs 采集的真实浏览器快照。
|
||||
*
|
||||
* 补丁项(capture-kepler-env 证明读取):
|
||||
* canvas 2d + webgl(指纹核心,jsdom 无)
|
||||
* navigator.platform/languages/maxTouchPoints/vendor/webdriver/...
|
||||
* screen 尺寸/色深,window 尺寸,plugins/mimeTypes,perf.now
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
|
||||
const BROWSER_ENV = {
|
||||
navigator: {
|
||||
platform: 'MacIntel',
|
||||
languages: ['zh-CN', 'zh'],
|
||||
hardwareConcurrency: 10,
|
||||
deviceMemory: 16,
|
||||
maxTouchPoints: 0,
|
||||
webdriver: false,
|
||||
vendor: 'Google Inc.',
|
||||
vendorSub: '',
|
||||
productSub: '20030107',
|
||||
onLine: true,
|
||||
pdfViewerEnabled: true,
|
||||
},
|
||||
screen: { width: 1440, height: 900, availWidth: 1440, availHeight: 900, colorDepth: 24, pixelDepth: 24 },
|
||||
window: { devicePixelRatio: 1, innerWidth: 1440, innerHeight: 900, outerWidth: 1442, outerHeight: 1026 },
|
||||
plugins: [
|
||||
['PDF Viewer', 'internal-pdf-viewer', 'Portable Document Format'],
|
||||
['Chrome PDF Viewer', 'internal-pdf-viewer', 'Portable Document Format'],
|
||||
['Chromium PDF Viewer', 'internal-pdf-viewer', 'Portable Document Format'],
|
||||
['Microsoft Edge PDF Viewer', 'internal-pdf-viewer', 'Portable Document Format'],
|
||||
['WebKit built-in PDF', 'internal-pdf-viewer', 'Portable Document Format'],
|
||||
],
|
||||
mimeTypes: [
|
||||
['application/pdf', 'Portable Document Format', ['pdf']],
|
||||
['text/pdf', 'Portable Document Format', ['pdf']],
|
||||
],
|
||||
// 常见 macOS 字体(measureText 字体探测用)
|
||||
fonts: ['Arial', 'Helvetica', 'Times New Roman', 'Courier New', 'Verdana', 'Georgia',
|
||||
'Trebuchet MS', 'Comic Sans MS', 'Palatino Linotype', 'Book Antiqua', 'Tahoma',
|
||||
'PingFang SC', 'PingFang HK', 'PingFang TC', 'Microsoft YaHei', 'SimSun', 'SimHei',
|
||||
'Hiragino Sans GB', 'Hiragino Kaku Gothic ProN', 'Menlo', 'Monaco', 'Consolas',
|
||||
'Lucida Console', 'Arial Black', 'Impact', 'Lucida Sans Unicode'],
|
||||
};
|
||||
|
||||
// 浏览器真实 canvas 64x64 像素 + toDataURL(capture 自真实 Chrome,注入 mock 用)
|
||||
let BROWSER_CANVAS = null;
|
||||
try {
|
||||
BROWSER_CANVAS = JSON.parse(fs.readFileSync('/tmp/browser-canvas64.json', 'utf8'));
|
||||
} catch (e) {}
|
||||
|
||||
// 固定 canvas 输出(浏览器 240x60 指纹画布 toDataURL 约 13762 字符,生成一个同量级固定 PNG)
|
||||
function makeCanvasDataURL() {
|
||||
// 构造浏览器同量级、高熵(类真实 PNG)的 data URL,长度 13762 附近
|
||||
const target = 13762;
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
let s = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAAD';
|
||||
while (s.length < target) {
|
||||
// 用随机高熵块(基于可复现伪随机,避免每次全变导致调试困难)
|
||||
let seed = s.length * 2654435761 >>> 0;
|
||||
for (let i = 0; i < 16 && s.length < target; i++) {
|
||||
seed = (seed * 1103515245 + 12345) >>> 0;
|
||||
s += chars[seed % 64];
|
||||
}
|
||||
}
|
||||
s = s.slice(0, target);
|
||||
return s;
|
||||
}
|
||||
|
||||
export function patchEnvironment(window) {
|
||||
const klog = (k, v) => { try { if (window.__klog && window.__klog.length < 5000) window.__klog.push(k + '=' + String(v).slice(0, 80)); } catch(e){} };
|
||||
// Encoding API:kepler 探测 typeof window.TextEncoder(浏览器 "function",jsdom 缺)
|
||||
if (typeof window.TextEncoder === 'undefined' && typeof globalThis.TextEncoder === 'function') {
|
||||
window.TextEncoder = globalThis.TextEncoder;
|
||||
try { window.TextEncoder.prototype = globalThis.TextEncoder.prototype; } catch (e) {}
|
||||
}
|
||||
if (typeof window.TextDecoder === 'undefined' && typeof globalThis.TextDecoder === 'function') {
|
||||
window.TextDecoder = globalThis.TextDecoder;
|
||||
try { window.TextDecoder.prototype = globalThis.TextDecoder.prototype; } catch (e) {}
|
||||
}
|
||||
// Web Audio mock:kepler 仅探测 OfflineAudioContext 存在 + sampleRate + destination.maxChannelCount + createOscillator(浏览器实测 8 条调用)
|
||||
if (typeof window.OfflineAudioContext === 'undefined') {
|
||||
const mkAudioParam = (defaultValue, minValue, maxValue, writable) => {
|
||||
const p = {
|
||||
defaultValue, minValue, maxValue, automationRate: 'a-rate',
|
||||
cancelScheduledValues() { return this; }, setValueAtTime() { return this; },
|
||||
linearRampToValueAtTime() { return this; }, exponentialRampToValueAtTime() { return this; },
|
||||
setTargetAtTime() { return this; }, setValueCurveAtTime() { return this; },
|
||||
cancelAndHoldAtTime() { return this; },
|
||||
};
|
||||
if (writable) {
|
||||
// Chrome 150 实测:DynamicsCompressorNode 的 AudioParam.value 可写且钳制到 [min,max]
|
||||
let val = defaultValue;
|
||||
Object.defineProperty(p, 'value', {
|
||||
get() { return val; },
|
||||
set(v) { val = Math.min(maxValue, Math.max(minValue, v)); },
|
||||
enumerable: true, configurable: true,
|
||||
});
|
||||
} else {
|
||||
// Chrome 150 实测:OscillatorNode 的 frequency/detune 为 getter-only,严格模式赋值抛 TypeError
|
||||
Object.defineProperty(p, 'value', {
|
||||
get() { return defaultValue; },
|
||||
set() { throw new TypeError('Cannot set property value of #<AudioParam> which has only a getter'); },
|
||||
enumerable: true, configurable: true,
|
||||
});
|
||||
}
|
||||
return p;
|
||||
};
|
||||
const mockNode = () => ({
|
||||
type: 'sine', value: 0, gain: { value: 1 },
|
||||
frequency: mkAudioParam(440, -22050, 22050),
|
||||
detune: mkAudioParam(0, -153600, 153600),
|
||||
connect() {}, disconnect() {}, start() {}, stop() {}, addEventListener() {}, removeEventListener() {},
|
||||
});
|
||||
class MockOfflineAudioContext {
|
||||
constructor(channels, length, sampleRate) {
|
||||
this.channels = channels;
|
||||
this.length = length;
|
||||
this.sampleRate = sampleRate || 44100;
|
||||
this.destination = { maxChannelCount: 2, channelCount: 2, channelCountMode: 'explicit' };
|
||||
this.currentTime = 0;
|
||||
this.__listeners = {};
|
||||
// 浏览器实测:startRendering() 后 renderedBuffer 为 AudioBuffer;kepler 读它做音频指纹
|
||||
Object.defineProperty(this, 'renderedBuffer', {
|
||||
get: () => ({
|
||||
getChannelData: () => new Float32Array(1),
|
||||
length: 1, numberOfChannels: 1, sampleRate: this.sampleRate || 44100, duration: 1 / (this.sampleRate || 44100),
|
||||
}),
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
addEventListener(type, fn) {
|
||||
(this.__listeners[type] = this.__listeners[type] || []).push(fn);
|
||||
}
|
||||
removeEventListener(type, fn) {
|
||||
const arr = this.__listeners[type] || [];
|
||||
const i = arr.indexOf(fn);
|
||||
if (i !== -1) arr.splice(i, 1);
|
||||
}
|
||||
dispatchEvent(ev) {
|
||||
const arr = this.__listeners[ev.type] || [];
|
||||
for (const fn of arr.slice()) fn.call(this, ev);
|
||||
return true;
|
||||
}
|
||||
createOscillator() { return mockNode(); }
|
||||
createGain() { return mockNode(); }
|
||||
createDynamicsCompressor() {
|
||||
// Chrome 150 实测值(capture-kepler-env 采集)
|
||||
return {
|
||||
threshold: mkAudioParam(-24, -100, 0, true),
|
||||
knee: mkAudioParam(30, 0, 40, true),
|
||||
ratio: mkAudioParam(12, 1, 20, true),
|
||||
attack: mkAudioParam(0.003000000026077032, 0, 1, true),
|
||||
release: mkAudioParam(0.25, 0, 1, true),
|
||||
connect() {}, disconnect() {}, addEventListener() {}, removeEventListener() {},
|
||||
};
|
||||
}
|
||||
createAnalyser() { return mockNode(); }
|
||||
createBiquadFilter() { return mockNode(); }
|
||||
createBuffer() { return { getChannelData: () => new Float32Array(0), numberOfChannels: 1, length: 0, sampleRate: this.sampleRate }; }
|
||||
startRendering() {
|
||||
// Chrome 行为:渲染完成后派发 oncomplete 事件(OfflineAudioCompletionEvent,带 renderedBuffer)
|
||||
const buf = this.renderedBuffer;
|
||||
Promise.resolve().then(() => {
|
||||
const ev = { renderedBuffer: buf, target: this, currentTarget: this, type: 'complete', timeStamp: Date.now() };
|
||||
if (typeof this.oncomplete === 'function') {
|
||||
try {
|
||||
this.oncomplete(ev);
|
||||
} catch (e) {}
|
||||
}
|
||||
try { this.dispatchEvent(ev); } catch (e) {}
|
||||
});
|
||||
return Promise.resolve(buf);
|
||||
}
|
||||
suspend() { return Promise.resolve(); }
|
||||
resume() { return Promise.resolve(); }
|
||||
close() { return Promise.resolve(); }
|
||||
}
|
||||
window.OfflineAudioContext = MockOfflineAudioContext;
|
||||
window.AudioContext = MockOfflineAudioContext;
|
||||
window.webkitOfflineAudioContext = MockOfflineAudioContext;
|
||||
}
|
||||
const nav = window.navigator;
|
||||
// 带读取日志的 getter(kepler 读取时记录,便于与浏览器对比)
|
||||
for (const [k, v] of Object.entries(BROWSER_ENV.navigator)) {
|
||||
try { Object.defineProperty(nav, k, { get: () => { klog('navigator.' + k, v); return v; }, configurable: true }); } catch (e) {}
|
||||
}
|
||||
|
||||
const scr = window.screen;
|
||||
for (const k of ['width', 'height', 'availWidth', 'availHeight', 'colorDepth', 'pixelDepth']) {
|
||||
try { Object.defineProperty(scr, k, { get: () => { klog('screen.' + k, BROWSER_ENV.screen[k]); return BROWSER_ENV.screen[k]; }, configurable: true }); } catch (e) {}
|
||||
}
|
||||
for (const k of ['devicePixelRatio', 'innerWidth', 'innerHeight', 'outerWidth', 'outerHeight']) {
|
||||
try { Object.defineProperty(window, k, { get: () => { klog('window.' + k, BROWSER_ENV.window[k]); return BROWSER_ENV.window[k]; }, configurable: true }); } catch (e) {}
|
||||
}
|
||||
|
||||
// plugins / mimeTypes(只读对象,支持索引与 length)
|
||||
const mkPluginArray = (items) => {
|
||||
const arr = items.map(([name, filename, description], i) => {
|
||||
const p = {
|
||||
name, filename, description,
|
||||
length: 0, item: (j) => arr[j] || null, namedItem: (n) => arr.find(x => x.name === n) || null,
|
||||
[Symbol.iterator]: function* () { yield* arr; },
|
||||
refresh: () => {},
|
||||
};
|
||||
p.index = i;
|
||||
return p;
|
||||
});
|
||||
Object.defineProperty(arr, 'length', { value: items.length });
|
||||
arr.item = (j) => arr[j] || null;
|
||||
arr.namedItem = (n) => arr.find(x => x.name === n) || null;
|
||||
arr.refresh = () => {};
|
||||
arr[Symbol.iterator] = function* () { yield* arr; };
|
||||
return arr;
|
||||
};
|
||||
const plugins = mkPluginArray(BROWSER_ENV.plugins.map(([n, f, d]) => [n, f, d]));
|
||||
const mimes = mkPluginArray(BROWSER_ENV.mimeTypes.map(([t, d, s]) => [t, d, s.join(',')]));
|
||||
Object.defineProperty(nav, 'plugins', { value: plugins });
|
||||
Object.defineProperty(nav, 'mimeTypes', { value: mimes });
|
||||
|
||||
// document.fonts(FontFaceSet 简化实现,支持 entries/keys/values/forEach/check/size)
|
||||
const fontList = BROWSER_ENV.fonts.map(f => ({
|
||||
family: f, weight: '400', style: 'normal', status: 'loaded',
|
||||
}));
|
||||
const fontSet = {
|
||||
size: fontList.length,
|
||||
status: 'loaded',
|
||||
ready: new Promise((res) => res({})),
|
||||
entries: () => fontList.entries(),
|
||||
keys: function* () { for (const f of fontList) yield f.family; },
|
||||
values: function* () { for (const f of fontList) yield f; },
|
||||
forEach: (cb) => fontList.forEach(cb),
|
||||
check: (font, text) => true,
|
||||
load: (font, text) => Promise.resolve([]),
|
||||
add: () => {}, delete: () => {},
|
||||
[Symbol.iterator]: function* () { yield* fontList; },
|
||||
};
|
||||
try { Object.defineProperty(window.document, 'fonts', { value: fontSet, configurable: true }); } catch (e) {}
|
||||
|
||||
// ---- performance 资源条目(kepler 编码资源时序;jsdom 默认无) ----
|
||||
const perfEntries = [
|
||||
['https://pay.qq.com/midas/minipay_v2/views/cpay/goods.shtml', 'navigation', 132, 0],
|
||||
['https://pub.idqqimg.com/pc/misc/sentry/raven.min.js', 'script', 3, 0],
|
||||
['https://midas.gtimg.cn/midas/minipay_v2/js/vendor.35cb3bc7d38036fed653.js', 'script', 85, 0],
|
||||
['https://midas.gtimg.cn/midas/minipay_v2/js/cgiVendor.53275fa063461d28e404.js', 'script', 41, 0],
|
||||
['https://midas.gtimg.cn/midas/minipay_v2/js/app/goodsBiz.47e6a80d478ce0bdfd50..js', 'script', 156, 0],
|
||||
['https://midas.gtimg.cn/midas/minipay_v2/js/app/goods.e87ce812409f000bf518.js', 'script', 210, 0],
|
||||
['https://midas.gtimg.cn/midas/minipay_v2/css/goods.e87ce812409f000bf518.css', 'link', 24, 0],
|
||||
['https://midas.gtimg.cn/midas/minipay_v2/css/vendor.35cb3bc7d38036fed653.css', 'link', 12, 0],
|
||||
['https://api.unipay.qq.com/v1/r/1450243039/web_page_info', 'xmlhttprequest', 88, 0],
|
||||
['https://api.unipay.qq.com/cgi-bin/fp-behv.fcg', 'xmlhttprequest', 2, 0],
|
||||
['https://xui.ptlogin2.qq.com/js/ptlogin_v1.js', 'script', 67, 0],
|
||||
['https://midas.gtimg.cn/store_config/1561715149975dieRkvjp.png', 'img', 18, 0],
|
||||
].map(([name, initiatorType, duration, transferSize]) => ({
|
||||
name, initiatorType, duration, transferSize, startTime: 0, responseEnd: duration, connectEnd: duration, domContentLoadedEventEnd: 0, loadEventEnd: 0,
|
||||
}));
|
||||
const perf = window.performance;
|
||||
try {
|
||||
perf.getEntries = () => perfEntries.slice();
|
||||
perf.getEntriesByType = (t) => t === 'resource' ? perfEntries.filter(e => e.initiatorType !== 'navigation') : (t === 'navigation' ? perfEntries.filter(e => e.initiatorType === 'navigation') : []);
|
||||
perf.timeOrigin = 1786000000000;
|
||||
if (!('getEntries' in perf) ) { /* ensure own prop */ }
|
||||
} catch (e) {}
|
||||
|
||||
// ---- canvas 2d mock(记录绘制,提供固定 measureText/toDataURL/getImageData) ----
|
||||
const makeCtx = (canvas) => {
|
||||
const mark = (m, ...a) => klog('mock2d.' + m, a.map(x => String(x).slice(0, 30)).join('|'));
|
||||
const ctx = {
|
||||
canvas,
|
||||
fillStyle: '#000', strokeStyle: '#000', font: '10px sans-serif', textBaseline: 'alphabetic',
|
||||
globalAlpha: 1, globalCompositeOperation: 'source-over', lineWidth: 1, lineCap: 'butt',
|
||||
shadowBlur: 0, shadowColor: 'rgba(0,0,0,0)', shadowOffsetX: 0, shadowOffsetY: 0,
|
||||
measureText: (t) => {
|
||||
mark('measureText', t);
|
||||
// 字体相关宽度(kepler 字体枚举依赖:不同字体宽度不同)
|
||||
const font = ctx.font || '10px sans-serif';
|
||||
const fam = String(font).split(/\s+/).pop() || 'sans-serif';
|
||||
let seed = 5381;
|
||||
for (const ch of fam) seed = ((seed * 33) ^ ch.charCodeAt(0)) >>> 0;
|
||||
const fontDelta = (seed % 73) / 2; // 0..36px,显著差异
|
||||
const hasEmoji = /[\uD800-\uDBFF]|[\u00C0-\u024F]|¯/.test(String(t));
|
||||
const emojiDelta = hasEmoji ? (seed % 17) : 0;
|
||||
return { width: String(t).length * 7.2 + fontDelta + emojiDelta, actualBoundingBoxAscent: 10, actualBoundingBoxDescent: 3 };
|
||||
},
|
||||
getImageData: (x, y, w, h) => {
|
||||
mark('getImageData', x, y, w, h);
|
||||
try { if (window.__itrace) window.__gdAt = window.__itrace.length - 1; } catch (e) {}
|
||||
if (BROWSER_CANVAS && BROWSER_CANVAS.data && w === 64 && h === 64) {
|
||||
return { width: 64, height: 64, data: new Uint8ClampedArray(BROWSER_CANVAS.data) };
|
||||
}
|
||||
// 浏览器式像素:平滑全渐变(每像素颜色不同,模拟真实渲染的色彩丰富度)
|
||||
const data = new Uint8ClampedArray(w * h * 4);
|
||||
for (let py = 0; py < h; py++) {
|
||||
for (let px = 0; px < w; px++) {
|
||||
const i = (py * w + px) * 4;
|
||||
// 平滑渐变:每像素的 RGB 随位置连续变化,产生大量不同颜色
|
||||
const r = (px * 255 / w) & 0xff;
|
||||
const g = (py * 255 / h) & 0xff;
|
||||
const b = ((px + py) * 255 / (w + h)) & 0xff;
|
||||
data[i] = r; data[i+1] = g; data[i+2] = b;
|
||||
data[i+3] = 255 - ((px * 3 + py) % 80); // alpha 也变化
|
||||
}
|
||||
}
|
||||
const img = { width: w, height: h, data };
|
||||
return new Proxy(img, {
|
||||
get(t, k) {
|
||||
if (k === 'data') klog('imgData.data', 'len=' + t.data.length);
|
||||
else if (k === 'width' || k === 'height') klog('imgData.' + k, t[k]);
|
||||
return t[k];
|
||||
},
|
||||
});
|
||||
},
|
||||
createImageData: (w, h) => ({ width: w, height: h, data: new Uint8ClampedArray(w * h * 4) }),
|
||||
createLinearGradient: () => ({ addColorStop: () => {} }),
|
||||
createRadialGradient: () => ({ addColorStop: () => {} }),
|
||||
createPattern: () => ({ setTransform: () => {} }),
|
||||
toDataURL: (type, q) => {
|
||||
mark('toDataURL');
|
||||
if (BROWSER_CANVAS && BROWSER_CANVAS.toDataURL) return BROWSER_CANVAS.toDataURL;
|
||||
const c = (canvas.__tdc = (canvas.__tdc || 0) + 1);
|
||||
const cc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'[c % 32];
|
||||
return canvas.__dataURL.slice(0, canvas.__dataURL.length - 1 - (c % 16)) + cc + canvas.__dataURL.slice(canvas.__dataURL.length - (c % 16));
|
||||
},
|
||||
getTransform: () => ({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }),
|
||||
setTransform: (...a) => { mark('setTransform'); }, resetTransform: () => { mark('resetTransform'); }, transform: () => { mark('transform'); }, translate: (...a) => { mark('translate', a[0], a[1]); },
|
||||
rotate: (a) => { mark('rotate', a); }, scale: (...a) => { mark('scale', a[0], a[1]); }, save: () => { mark('save'); }, restore: () => { mark('restore'); }, beginPath: () => { mark('beginPath'); },
|
||||
closePath: () => { mark('closePath'); }, moveTo: (...a) => { mark('moveTo', a[0], a[1]); }, lineTo: (...a) => { mark('lineTo', a[0], a[1]); }, bezierCurveTo: () => { mark('bezierCurveTo'); },
|
||||
quadraticCurveTo: () => { mark('quadraticCurveTo'); }, arc: (...a) => { mark('arc', a[0], a[1], a[2]); }, arcTo: () => { mark('arcTo'); }, rect: (...a) => { mark('rect', a[0], a[1], a[2], a[3]); }, fill: (...a) => { mark('fill', a[0]); },
|
||||
stroke: () => { mark('stroke'); }, clip: () => { mark('clip'); }, fillRect: (...a) => { mark('fillRect', a[0], a[1], a[2], a[3]); }, strokeRect: () => { mark('strokeRect'); }, clearRect: () => { mark('clearRect'); },
|
||||
fillText: (...a) => { mark('fillText', a[0], a[1], a[2]); }, strokeText: (...a) => { mark('strokeText', a[0]); }, drawImage: () => { mark('drawImage'); }, putImageData: () => { mark('putImageData'); },
|
||||
isPointInPath: () => false, isPointInStroke: () => false,
|
||||
};
|
||||
return ctx;
|
||||
};
|
||||
|
||||
// ---- webgl mock(kepler 调 getContext('webgl') + toDataURL) ----
|
||||
const makeWebGL = (canvas) => ({
|
||||
canvas,
|
||||
drawingBufferWidth: 240, drawingBufferHeight: 60,
|
||||
getParameter: (p) => {
|
||||
klog('webgl.getParameter', p);
|
||||
const map = {
|
||||
7936: 'WebKit', 7937: 'WebGL 1.0 (OpenGL ES 2.0 Chromium)', 7938: 'WebGL 1.0',
|
||||
35724: 'WebGL', 37445: 'Google Inc. (NVIDIA)', 37446: 'ANGLE (Apple, Apple M1, OpenGL 4.1)',
|
||||
3379: 64, 34076: 16384, 34921: 0, 36347: 8192,
|
||||
};
|
||||
return map[p] !== undefined ? map[p] : 0;
|
||||
},
|
||||
getExtension: (name) => {
|
||||
klog('webgl.getExtension', name);
|
||||
return name === 'WEBGL_debug_renderer_info' ? { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 } : null;
|
||||
},
|
||||
getSupportedExtensions: () => ['ANGLE_instanced_arrays', 'EXT_blend_minmax', 'EXT_texture_filter_anisotropic', 'OES_element_index_uint', 'OES_standard_derivatives', 'WEBGL_debug_renderer_info'],
|
||||
getContextAttributes: () => ({ alpha: true, antialias: true, depth: true, failIfMajorPerformanceCaveat: false, premultipliedAlpha: true, preserveDrawingBuffer: false, stencil: false, powerPreference: 'default' }),
|
||||
readPixels: () => {}, getUniformLocation: () => ({}), createBuffer: () => ({}),
|
||||
createShader: () => ({}), createProgram: () => ({}), getShaderParameter: () => true,
|
||||
getProgramParameter: () => true, bindBuffer: () => {}, bufferData: () => {},
|
||||
shaderSource: () => {}, compileShader: () => {}, attachShader: () => {},
|
||||
linkProgram: () => {}, useProgram: () => {}, vertexAttribPointer: () => {},
|
||||
enableVertexAttribArray: () => {}, drawArrays: () => {}, viewport: () => {},
|
||||
clearColor: () => {}, clear: () => {}, enable: () => {}, disable: () => {},
|
||||
texImage2D: () => {}, texParameteri: () => {}, activeTexture: () => {}, bindTexture: () => {},
|
||||
uniform1f: () => {}, uniform2f: () => {}, uniform3f: () => {}, uniform1i: () => {},
|
||||
getAttribLocation: () => 0, getError: () => 0, getShaderInfoLog: () => '', getProgramInfoLog: () => '',
|
||||
pixelStorei: () => {}, colorMask: () => {}, depthMask: () => {}, depthFunc: () => {},
|
||||
blendFunc: () => {}, cullFace: () => {}, frontFace: () => {}, lineWidth: () => {},
|
||||
getFramebufferAttachmentParameter: () => null, isContextLost: () => false,
|
||||
getContextAttributes: () => ({}), loseContext: () => {}, restoreContext: () => {},
|
||||
getParameterWithDefault: () => 0,
|
||||
});
|
||||
|
||||
const canvasDataURL = makeCanvasDataURL();
|
||||
const getContextOrig = window.HTMLCanvasElement.prototype.getContext;
|
||||
// jsdom canvas 默认尺寸改 64x64(浏览器 kepler 指纹画布尺寸)
|
||||
try {
|
||||
Object.defineProperty(window.HTMLCanvasElement.prototype, 'width', {
|
||||
get() { return 64; }, set() {}, configurable: true,
|
||||
});
|
||||
Object.defineProperty(window.HTMLCanvasElement.prototype, 'height', {
|
||||
get() { return 64; }, set() {}, configurable: true,
|
||||
});
|
||||
} catch (e) {}
|
||||
window.HTMLCanvasElement.prototype.getContext = function (type) {
|
||||
if (type === '2d') {
|
||||
if (!this.__ctx2d) {
|
||||
this.__ctx2d = makeCtx(this);
|
||||
Object.defineProperty(this, '__dataURL', { value: canvasDataURL, configurable: true });
|
||||
}
|
||||
return this.__ctx2d;
|
||||
}
|
||||
if (type === 'webgl' || type === 'experimental-webgl') {
|
||||
if (!this.__webgl) {
|
||||
this.__webgl = makeWebGL(this);
|
||||
Object.defineProperty(this, '__dataURL', { value: canvasDataURL, configurable: true });
|
||||
}
|
||||
return this.__webgl;
|
||||
}
|
||||
return getContextOrig ? getContextOrig.apply(this, arguments) : null;
|
||||
};
|
||||
Object.defineProperty(window.HTMLCanvasElement.prototype, 'toDataURL', {
|
||||
value: function () { return canvasDataURL; },
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user