diff --git a/src/core/event.js b/src/core/event.js index 46993fe45..f25165988 100644 --- a/src/core/event.js +++ b/src/core/event.js @@ -4,20 +4,30 @@ import Eventful from '../mixin/Eventful'; import env from './env'; +import {buildTransformer} from './fourPointsTransform'; var isDomLevel2 = (typeof window !== 'undefined') && !!window.addEventListener; var MOUSE_EVENT_REG = /^(?:mouse|pointer|contextmenu|drag|drop)|click/; - -function getBoundingClientRect(el) { - // BlackBerry 5, iOS 3 (original iPhone) don't have getBoundingRect - return el.getBoundingClientRect ? el.getBoundingClientRect() : {left: 0, top: 0}; -} +var EVENT_SAVED_PROP = '___zrEVENTSAVED'; +var _calcOut = []; /** * Get the `zrX` and `zrY`, which are relative to the top-left of * the input `el`. - * CSS transform is supported. + * CSS transform (2D & 3D) is supported. + * + * The strategy to fetch the coords: + * + If `calculate` is not set as `true`, users of this method should + * ensure that `el` is the same or the same size & location as `e.target`. + * Otherwise the result coords are probably not expected. Because we + * firstly try to get coords from e.offsetX/e.offsetY. + * + If `calculate` is set as `true`, the input `el` can be any element + * and we force to calculate the coords based on `el`. + * + The input `el` should be positionable (not position:static). + * + * The force `calculate` can be used in case like: + * When mousemove event triggered on ec tooltip, `e.target` is not `el`(zr painter.dom). * * @param {HTMLElement} el DOM element. * @param {Event} e Mouse event or touch event. @@ -35,12 +45,8 @@ export function clientToLocal(el, e, out, calculate) { // (see http://www.jacklmoore.com/notes/mouse-position/) // In zr painter.dom, padding edge equals to border edge. - // FIXME - // When mousemove event triggered on ec tooltip, target is not zr painter.dom, and - // offsetX/Y is relative to e.target, where the calculation of zrX/Y via offsetX/Y - // is too complex. So css-transfrom dont support in this case temporarily. if (calculate || !env.canvasSupported) { - defaultGetZrXY(el, e, out); + calculateZrXY(el, e, out); } // Caution: In FireFox, layerX/layerY Mouse position relative to the closest positioned // ancestor element, so we should make sure el is positioned (e.g., not position:static). @@ -60,29 +66,119 @@ export function clientToLocal(el, e, out, calculate) { } // For some other device, e.g., IOS safari. else { - defaultGetZrXY(el, e, out); + calculateZrXY(el, e, out); } return out; } -function defaultGetZrXY(el, e, out) { - // This well-known method below does not support css transform. - var box = getBoundingClientRect(el); - out.zrX = e.clientX - box.left; - out.zrY = e.clientY - box.top; +function calculateZrXY(el, e, out) { + // BlackBerry 5, iOS 3 (original iPhone) don't have getBoundingRect. + if (el.getBoundingClientRect && env.domSupported) { + var ex = e.clientX; + var ey = e.clientY; + + if (el.nodeName.toUpperCase() === 'CANVAS') { + // Original approach, which do not support CSS transform. + // marker can not be locationed in a canvas container + // (getBoundingClientRect is always 0). We do not support + // that input a pre-created canvas to zr while using css + // transform in iOS. + var box = el.getBoundingClientRect(); + out.zrX = ex - box.left; + out.zrY = ey - box.top; + return; + } + else { + var saved = el[EVENT_SAVED_PROP] || (el[EVENT_SAVED_PROP] = {}); + var transformer = preparePointerTransformer(prepareCoordMarkers(el, saved), saved); + if (transformer) { + transformer(_calcOut, ex, ey); + out.zrX = _calcOut[0]; + out.zrY = _calcOut[1]; + return; + } + } + } + out.zrX = out.zrY = 0; +} + +function prepareCoordMarkers(el, saved) { + var markers = saved.markers; + if (markers) { + return markers; + } + + markers = saved.markers = []; + var propLR = ['left', 'right']; + var propTB = ['top', 'bottom']; + + for (var i = 0; i < 4; i++) { + var marker = document.createElement('div'); + var stl = marker.style; + var idxLR = i % 2; + var idxTB = (i >> 1) % 2; + stl.cssText = [ + 'position:absolute', + 'visibility: hidden', + 'padding: 0', + 'margin: 0', + 'border-width: 0', + 'width:0', + 'height:0', + // 'width: 5px', + // 'height: 5px', + propLR[idxLR] + ':0', + propTB[idxTB] + ':0', + propLR[1 - idxLR] + ':auto', + propTB[1 - idxTB] + ':auto', + '' + ].join('!important;'); + el.appendChild(marker); + markers.push(marker); + } + + return markers; +} + +function preparePointerTransformer(markers, saved) { + var transformer = saved.transformer; + var oldSrcCoords = saved.srcCoords; + var useOld = true; + var srcCoords = []; + var destCoords = []; + + for (var i = 0; i < 4; i++) { + var rect = markers[i].getBoundingClientRect(); + var ii = 2 * i; + var x = rect.left; + var y = rect.top; + srcCoords.push(x, y); + useOld &= oldSrcCoords && x === oldSrcCoords[ii] && y === oldSrcCoords[ii + 1]; + destCoords.push(markers[i].offsetLeft, markers[i].offsetTop); + } + + // Cache to avoid time consuming of `buildTransformer`. + return useOld + ? transformer + : ( + saved.srcCoords = srcCoords, + saved.transformer = buildTransformer(srcCoords, destCoords) + ); } /** * Normalize the coordinates of the input event. * * Get the `e.zrX` and `e.zrY`, which are relative to the top-left of - * the input `el`. CSS transform is supported. + * the input `el`. * Get `e.zrDelta` if using mouse wheel. * Get `e.which`, see the comment inside this function. * * Do not calculate repeatly if `zrX` and `zrY` already exist. - * CSS transform is supported. + * + * Notice: see comments in `clientToLocal`. check the relationship + * between the result coords and the parameters `el` and `calculate`. * * @param {HTMLElement} el DOM element. * @param {Event} [e] Mouse event or touch event. For lagency IE, diff --git a/src/core/fourPointsTransform.js b/src/core/fourPointsTransform.js new file mode 100644 index 000000000..62149fb2d --- /dev/null +++ b/src/core/fourPointsTransform.js @@ -0,0 +1,99 @@ +/** + * The algoritm is learnt from + * https://franklinta.com/2014/09/08/computing-css-matrix3d-transforms/ + * And we made some optimization for matrix inversion. + * Other similar approaches: + * "cv::getPerspectiveTransform", "Direct Linear Transformation". + */ + +var LN2 = Math.log(2); + +function determinant(rows, rank, rowStart, rowMask, colMask, detCache) { + var cacheKey = rowMask + '-' + colMask; + var fullRank = rows.length; + + if (detCache.hasOwnProperty(cacheKey)) { + return detCache[cacheKey]; + } + + if (rank === 1) { + // In this case the colMask must be like: `11101111`. We can find the place of `0`. + var colStart = Math.round(Math.log(((1 << fullRank) - 1) & ~colMask) / LN2); + return rows[rowStart][colStart]; + } + + var subRowMask = rowMask | (1 << rowStart); + var subRowStart = rowStart + 1; + while (rowMask & (1 << subRowStart)) { + subRowStart++; + } + + var sum = 0; + for (var j = 0, colLocalIdx = 0; j < fullRank; j++) { + var colTag = 1 << j; + if (!(colTag & colMask)) { + sum += (colLocalIdx % 2 ? -1 : 1) * rows[rowStart][j] + // det(subMatrix(0, j)) + * determinant(rows, rank - 1, subRowStart, subRowMask, colMask | colTag, detCache); + colLocalIdx++; + } + } + + detCache[cacheKey] = sum; + + return sum; +} + +/** + * Usage: + * ```js + * var transformer = buildTransformer( + * [10, 44, 100, 44, 100, 300, 10, 300], + * [50, 54, 130, 14, 140, 330, 14, 220] + * ); + * var out = []; + * transformer && transformer([11, 33], out); + * ``` + * + * Notice: `buildTransformer` may take more than 10ms in some Android device. + * + * @param {Array.} src source four points, [x0, y0, x1, y1, x2, y2, x3, y3] + * @param {Array.} dest destination four points, [x0, y0, x1, y1, x2, y2, x3, y3] + * @return {Function} transformer If fail, return null/undefined. + */ +export function buildTransformer(src, dest) { + var mA = [ + [src[0], src[1], 1, 0, 0, 0, -dest[0] * src[0], -dest[0] * src[1]], + [0, 0, 0, src[0], src[1], 1, -dest[1] * src[0], -dest[1] * src[1]], + [src[2], src[3], 1, 0, 0, 0, -dest[2] * src[2], -dest[2] * src[3]], + [0, 0, 0, src[2], src[3], 1, -dest[3] * src[2], -dest[3] * src[3]], + [src[4], src[5], 1, 0, 0, 0, -dest[4] * src[4], -dest[4] * src[5]], + [0, 0, 0, src[4], src[5], 1, -dest[5] * src[4], -dest[5] * src[5]], + [src[6], src[7], 1, 0, 0, 0, -dest[6] * src[6], -dest[6] * src[7]], + [0, 0, 0, src[6], src[7], 1, -dest[7] * src[6], -dest[7] * src[7]] + ]; + + var detCache = {}; + var det = determinant(mA, 8, 0, 0, 0, detCache); + if (det === 0) { + return; + } + + // `invert(mA) * dest`, that is, `adj(mA) / det * dest`. + var vh = []; + for (var i = 0; i < 8; i++) { + for (var j = 0; j < 8; j++) { + vh[j] == null && (vh[j] = 0); + vh[j] += ((i + j) % 2 ? -1 : 1) + // det(subMatrix(i, j)) + * determinant(mA, 7, i === 0 ? 1 : 0, 1 << i, 1 << j, detCache) + / det * dest[i]; + } + } + + return function (out, srcPointX, srcPointY) { + var pk = srcPointX * vh[6] + srcPointY * vh[7] + 1; + out[0] = (srcPointX * vh[0] + srcPointY * vh[1] + vh[2]) / pk; + out[1] = (srcPointX * vh[3] + srcPointY * vh[4] + vh[5]) / pk; + }; +} diff --git a/test/css-transform.html b/test/css-transform.html new file mode 100644 index 000000000..8b226c355 --- /dev/null +++ b/test/css-transform.html @@ -0,0 +1,567 @@ + + + + + + + + + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/lib/facePrint.js b/test/lib/facePrint.js new file mode 100644 index 000000000..e75ad9cd0 --- /dev/null +++ b/test/lib/facePrint.js @@ -0,0 +1,109 @@ + +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +// Just for temporarily mobile debug. + +(function () { + + var infoDom; + var msgs = []; + + var count = 0; + + /** + * @param {string|Object|Array} msg + */ + window.facePrint = function (msg) { + if (!infoDom) { + infoDom = createInfoDom(); + } + + if (isObject(msg)) { + msg = window.facePrint.objToStr(msg); + } + + msgs.push(encodeHTML(msg)); + count++; + + if (msgs.length > 30) { + msgs.shift(); + } + + var str = ''; + // Make some change in view, otherwise user may + // be not aware that log is still printing. + for (var i = 0; i < msgs.length; i++) { + str += '' + + (count - msgs.length + i) + '' + msgs[i]; + } + infoDom.innerHTML = str; + }; + + window.facePrint.objToStr = function (obj) { + var msgArr = []; + /* eslint-disable */ + for (var key in obj) { + msgArr.push(key + '=' + obj[key]); + } + /* eslint-enable */ + return msgArr.join(', '); + }; + + function createInfoDom() { + var dom = document.createElement('div'); + + dom.style.cssText = [ + 'position: fixed', + 'top: 0', + 'width: 100%', + 'min-height: 14px', + 'line-height: 14px', + 'z-index: 2147483647', + 'color: #fff', + 'font-size: 9px', + 'background: #000', + 'word-break:break-all', + 'word-wrap:break-word' + ].join(';') + ';'; + + document.body.appendChild(dom); + + return dom; + } + + function encodeHTML(source) { + return source == null + ? '' + : String(source) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + function isObject(value) { + // Avoid a V8 JIT bug in Chrome 19-20. + // See https://code.google.com/p/v8/issues/detail?id=2291 for more details. + var type = typeof value; + return type === 'function' || (!!value && type == 'object'); + } + +})(); \ No newline at end of file