使用CSS3 transform scale在一个点上进行缩放。
使用CSS3 transform scale在一个点上进行缩放。
尽管下面的代码片段看起来很简短,但我花了几天的时间(真丢人!)寻找只使用CSS3的transform
来实现点击缩放的方法。现在它可以工作了:\n
var current = {x: 0, y: 0, zoom: 1}, c = document.getElementById('container'); window.onclick = function(e) { wx = current.x + e.clientX / current.zoom; wy = current.y + e.clientY / current.zoom; var coef = e.ctrlKey ? 0.5 : 2; current.zoom *= coef; current.x = wx - e.clientX / current.zoom; current.y = wy - e.clientY / current.zoom; c.style.transform = 'scale(' + current.zoom +') translate(' + (-current.x) + 'px,' + (-current.y) + 'px)'; };
\n
html, body { margin: 0; padding: 0; overflow: hidden; min-height: 100%; } #container { position: absolute; transform-origin: 0 0; transition-duration: 3s;} #item { position: absolute; left:0px; top:0px; }
\n
\n唯一的问题是过渡效果很奇怪,就好像先平移后缩放一样;它产生了奇怪的锯齿效果。如何在这种情况下实现平滑的CSS3过渡效果?\n点击这里查看奇怪过渡效果的动态GIF:http://gget.it/zf3fmwum/weirdtransition.gif\n注:被点击的点是缩放变换的固定点(例如:点击眼睛,图像会缩放,光标仍然在眼睛上),就像在Google地图中双击缩放一样。
Zooming on a point with CSS3 transform scale是一个关于使用CSS3 transform scale进行点的缩放的问题。原因是作者想要分享自己成功校准缩放算法的经验。解决方法是通过创建一个viewer类来与底层图像进行交互。该解决方法的一个重要点是它不修改默认的transform-origin,这对于其他变换可能是有用的。
以下是问题的解决方法的代码:
var Point = (function () { function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function () { return '(' + this.x + ';' + this.y + ')'; }; return Point; })(); var Transform = (function () { function Transform() { this.translate = new Point(0, 0); this.scale = 1; this.angle = 0; } return Transform; })(); var UI; (function (UI) { var Viewer = (function () { function Viewer(viewer) { this.ticking = false; console.info("viewer browser on: " + viewer); this.viewer = viewer; this.viewer.style.position = 'relative'; this.viewer.style.overflow = 'hidden'; this.viewer.style.touchAction = 'none'; this.viewer.style.backgroundColor = '#000000'; this.viewer.style['-webkit-user-select'] = 'none'; this.viewer.style['-webkit-user-drag'] = 'none'; this.viewer.style['-webkit-tap-highlight-color'] = 'rgba(0, 0, 0, 0)'; this.viewerContent = this.viewer.querySelector(".image"); if (this.viewerContent == null) { this.viewerContent = document.createElement('img'); this.viewerContent.className = 'image'; this.viewer.appendChild(this.viewerContent); } this.viewerContent.style.position = 'absolute'; this.viewerContent.style.transition = 'transform 100ms linear'; console.info("image width = " + this.viewer.clientWidth + "x" + this.viewer.clientHeight); this.transform = new Transform(); this.initializeHammerEvents(); console.info("viewer controller constructed: " + this.transform); this.setViewPortSize({ width: this.viewer.clientWidth, height: this.viewer.clientHeight }); } Viewer.prototype.initializeHammerEvents = function () { var _this = this; this.gestureManager = new Hammer.Manager(this.viewer, { touchAction: 'pan-x pan-y' }); this.gestureManager.add(new Hammer.Pinch({ threshold: 0 })); this.gestureManager.on("pinchstart pinchmove", function (event) { _this.onPinch(event); }); this.viewerContent.addEventListener("click", function (event) { _this.onImageClick(event); }); }; Viewer.prototype.setViewPortSize = function (size) { this.viewer.style.width = size.width + 'px'; this.viewer.style.height = size.height + 'px'; this.adjustZoom(); }; Viewer.prototype.getViewPortSize = function () { return { width: this.viewer.clientWidth, height: this.viewer.clientHeight }; }; Viewer.prototype.getDocumentSize = function () { return { width: this.viewerContent.clientWidth, height: this.viewerContent.clientHeight }; }; Viewer.prototype.setSource = function (source) { var _this = this; this.viewerContent.src = source; this.viewerContent.onload = function () { console.info("image loaded"); _this.adjustZoom(); }; }; Viewer.prototype.adjustZoom = function () { var size = this.getViewPortSize(); var documentSize = this.getDocumentSize(); console.info("adjust zoom, documentSize: " + documentSize.width + "x" + documentSize.height); console.info("adjust zoom, viewPortSize: " + size.width + "x" + size.height); this.minScale = 100 / documentSize.width; console.info("minScale=" + this.minScale); var widthScale = size.width / documentSize.width; var heightScale = size.height / documentSize.height; var scale = Math.min(widthScale, heightScale); var left = (size.width - documentSize.width) / 2; var top = (size.height - documentSize.height) / 2; console.log("setting content to : left => " + left + " , top => " + top, ", scale => ", scale); this.viewerContent.style.left = left + 'px'; this.viewerContent.style.top = top + 'px'; this.transform.scale = scale; this.updateElementTransform(); }; Viewer.prototype.onPinch = function (ev) { var pinchCenter = new Point(ev.center.x - this.viewer.offsetLeft, ev.center.y - this.viewer.offsetTop); console.info("pinch - center=" + pinchCenter + " scale=" + ev.scale); if (ev.type == 'pinchstart') { this.pinchInitialScale = this.transform.scale || 1; } var targetScale = this.pinchInitialScale * ev.scale; if (targetScale <= this.minScale) { targetScale = this.minScale; } if (Math.abs(this.transform.scale - this.minScale) < 1e-10 && Math.abs(targetScale - this.minScale) < 1e-10) { console.debug('already at min scale'); this.requestElementUpdate(); return; } this.zoomTo(new Point(ev.center.x, ev.center.y), targetScale); }; Viewer.prototype.onImageClick = function (event) { console.info("click"); var zoomCenter = new Point(event.pageX - this.viewer.offsetLeft, event.pageY - this.viewer.offsetTop); var scaleFactor = event.shiftKey || event.ctrlKey ? 0.75 : 1.25; this.zoomTo(zoomCenter, scaleFactor * this.transform.scale); }; Viewer.prototype.zoomTo = function (zoomCenter, newScale) { var viewPortSize = this.getViewPortSize(); var viewPortCenter = new Point(viewPortSize.width / 2, viewPortSize.height / 2); var zoomRelativeCenter = new Point(zoomCenter.x - viewPortCenter.x, zoomCenter.y - viewPortCenter.y); console.debug('clicked at ' + zoomRelativeCenter + ' (relative to center)'); var oldScale = this.transform.scale; // calculate translate difference // 1. center on new coordinates var zoomDx = -(zoomRelativeCenter.x) / oldScale; var zoomDy = -(zoomRelativeCenter.y) / oldScale; // 2. translate from center to clicked point with new zoom zoomDx += (zoomRelativeCenter.x) / newScale; zoomDy += (zoomRelativeCenter.y) / newScale; console.debug('dx=' + zoomDx + ' dy=' + zoomDy + ' oldScale=' + oldScale); /// move to the difference this.transform.translate.x += zoomDx; this.transform.translate.y += zoomDy; this.transform.scale = newScale; console.debug("applied zoom: scale= " + this.transform.scale + ' translate=' + this.transform.translate); this.requestElementUpdate(); }; Viewer.prototype.requestElementUpdate = function () { var _this = this; if (!this.ticking) { window.requestAnimationFrame(function () { _this.updateElementTransform(); }); this.ticking = true; } }; Viewer.prototype.updateElementTransform = function () { var value = [ 'rotate(' + this.transform.angle + 'deg)', 'scale(' + this.transform.scale + ', ' + this.transform.scale + ')', 'translate3d(' + this.transform.translate.x + 'px, ' + this.transform.translate.y + 'px, 0px)', ]; var stringValue = value.join(" "); console.debug("transform = " + stringValue); this.viewerContent.style.transform = stringValue; this.viewerContent.style.webkitTransform = stringValue; this.viewerContent.style.MozTransform = stringValue; this.viewerContent.style.msTransform = stringValue; this.viewerContent.style.OTransform = stringValue; this.ticking = false; }; return Viewer; })(); UI.Viewer = Viewer; })(UI || (UI = {}));
以上是Zooming on a point with CSS3 transform scale问题的原因和解决方法的整理。
Zooming on a point with CSS3 transform scale问题的出现的原因是在应用变换时,变换的顺序会影响结果。如果在缩放(scale)和平移(translate)之间调换顺序,你会发现示例的效果会有所不同。
解决方法是保持变换的顺序,确保先进行平移再进行缩放。这样可以避免出现问题。在给出的代码示例中,作者重新实现了一个可以正常工作的版本,并在变换顺序上进行了调整。
此外,还有一个性能优化的建议,即使用translate3D代替translate,因为在许多系统上它的性能更好。
在更新的代码中,作者还考虑了附加要求,并对计算进行了修改,以包括鼠标指针偏移的差异。这样可以保持缩放后的平移围绕鼠标指针进行。
最后,还有变换顺序的重要性,并给出了一个链接,详细解释了变换顺序的影响。同时,作者也指出了代码中的一个小错误,并将其进行了修正。
通过阅读上述内容,我们可以了解到Zooming on a point with CSS3 transform scale问题的出现原因以及解决方法。