【问题标题】:Zooming on a point with CSS3 transform scale使用 CSS3 变换比例缩放一个点
【发布时间】:2015-02-20 15:00:46
【问题描述】:

即使下面的代码 sn-p 看起来很短,我在几天内都在努力(真为我感到羞耻!)找到一种方法来缩放被点击的点,只使用 CSS3 transform。现在可以了:

    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)';
    };
    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; }
  <div id="container"><div id="item"><img src="http://fadili.users.greyc.fr/demos/WaveRestore/EMInpaint/parrot_original.png"></img></div></div>

唯一的问题是过渡很奇怪,就像它先平移然后缩放一样;它会产生一种奇怪的锯齿形效果。在这种情况下如何实现平滑的 CSS3 过渡?

在此处查看奇怪的过渡效果的动画 GIFhttp://gget.it/zf3fmwum/weirdtransition.gif

注意:点击的点是缩放变换的固定点(例如:点击眼睛,图片放大,光标还在眼睛上),就像在 GoogleMaps-doubleclick-zooming 中一样。

【问题讨论】:

  • 旁注:我尝试使用 transform-origin 失败:它让我想到了仿射变换的组合,这可能很棘手:如果你用因子 1/k 的仿射缩放组合因子 k 的仿射缩放结果不是仿射缩放,而是平移等(它给我带来了一些在一半时间内有效的糟糕代码)
  • scale3d jsfiddle.net/zpb5jxzw 试过了,有点效果
  • @VitorinoFernandes 问题没解决,转场还是怪怪的,看这里的动图:gget.it/zf3fmwum/weirdtransition.gif
  • 试一试:(1) 设置transition-property: none (2) 应用translate 转换 (3) 设置transition-property: all (4) 更新@ 987654335@ 变换。如果过渡仍然存在,请尝试在 #3 之后触发回流

标签: css transform css-transitions transition


【解决方案1】:

查看/缩放和平移图像是个棘手的问题,对吧? :)

我终于成功校准了缩放算法,所以我想与社区分享。我创建了一个查看器类来与底层图像交互。我的解决方案中的一个重点是它不会修改默认的转换原点,这可能对其他一些转换有用。

您可以使用 click 来缩放 / ctrl + click 来取消缩放,或捏合捏出(使用 Hammer JS)。警告,Firefox 默认不启用触摸事件。

对不起,我知道它使用 Hammer 和自制的 Transform & Point 类,但请关注 zoomTo 方法,它与框架无关,是这个缩放问题的重点。

(如果您愿意,可以在下面找到 TypeScript 版本)

在这个 sn-p 中尝试一下

// LOAD VIEWER
window.onload = function() {
var v = new UI.Viewer(document.getElementById('viewer'));
			v.setViewPortSize({width: 900, height: 600});
			v.setSource('https://upload.wikimedia.org/wikipedia/commons/d/d9/Big_Bear_Valley,_California.jpg');
}


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.enableGestures = function () {
            this.initializeHammerEvents();
            this.viewer.style.pointerEvents = 'auto';
        };
        Viewer.prototype.disableGestures = function () {
            this.viewer.style.pointerEvents = 'none';
            this.gestureManager.off('panstart panmove rotatestart rotatemove pinchstart pinchmove pinchend rotateend press doubletap');
        };
        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 = {}));
<!DOCTYPE html>
<html lang="fr">
	<head>
		<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
	</head>

	<body>
<br />
<br />
<br />
		<div id="viewer">
		</div>

		<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>

	</body>
</html>

TypeScript 版本

class Point {

    public x: number;
    public y: number;

    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }

    public toString(): string {
        return '(' + this.x + ';' + this.y + ')';
    }
}

interface Dimension {
    width: number;
    height: number;
}

class Transform {
    constructor() {
        this.translate = new Point(0, 0);
        this.scale = 1;
        this.angle = 0;
    }
    public translate: Point;
    public scale: number;
    public angle: number;
}

namespace UI {

    export class Viewer {

        private transform: Transform;
        private gestureManager: HammerManager;

        private viewer: HTMLDivElement;
        private viewerContent: HTMLImageElement;

        private ticking: boolean = false;

        private minScale: number;
        private pinchInitialScale: number;

        constructor(viewer: HTMLDivElement) {
            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 = <HTMLImageElement>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 });
        }

        public initializeHammerEvents(): void {

            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", (event) => { this.onPinch(event); });

            this.viewerContent.addEventListener("click", (event: MouseEvent) => {
                this.onImageClick(event);
            });
        }

        private enableGestures(): void {
            this.initializeHammerEvents();
            this.viewer.style.pointerEvents = 'auto';
        }

        private disableGestures(): void {
            this.viewer.style.pointerEvents = 'none';
            this.gestureManager.off('panstart panmove rotatestart rotatemove pinchstart pinchmove pinchend rotateend press doubletap');
        }

        public setViewPortSize(size: Dimension): void {
            this.viewer.style.width = size.width + 'px';
            this.viewer.style.height = size.height + 'px';

            this.adjustZoom();
        }

        public getViewPortSize(): Dimension {
            return {
                width: this.viewer.clientWidth,
                height: this.viewer.clientHeight
            };
        }

        public getDocumentSize(): Dimension {
            return {
                width: this.viewerContent.clientWidth,
                height: this.viewerContent.clientHeight
            };
        }

        public setSource(source: string): void {
            this.viewerContent.src = source;
            this.viewerContent.onload = () => {
                console.info("image loaded");
                this.adjustZoom();
            };
        }

        private adjustZoom(): void {

            var size: Dimension = this.getViewPortSize();
            var documentSize: Dimension = 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: number = size.width / documentSize.width;
            var heightScale: number = size.height / documentSize.height;
            var scale: number = Math.min(widthScale, heightScale);

            var left: number = (size.width - documentSize.width) / 2;
            var top: number = (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();
        }

        private onPinch(ev: HammerInput): void {

            var pinchCenter: Point = 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: number = 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);
        }

        private onImageClick(event: MouseEvent) {
            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);
        }

        private zoomTo(zoomCenter: Point, newScale: number): void {
            var viewPortSize: Dimension = this.getViewPortSize();
            var viewPortCenter: Point = new Point(viewPortSize.width / 2, viewPortSize.height / 2);

            var zoomRelativeCenter: Point = new Point(zoomCenter.x - viewPortCenter.x, zoomCenter.y - viewPortCenter.y);
            console.debug('clicked at ' + zoomRelativeCenter + ' (relative to center)');

            var oldScale: number = this.transform.scale;

            // calculate translate difference 

            // 1. center on new coordinates
            var zoomDx: number = -(zoomRelativeCenter.x) / oldScale;
            var zoomDy: number = -(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();
        }

        private requestElementUpdate() {
            if (!this.ticking) {
                window.requestAnimationFrame(() => { this.updateElementTransform() });
                this.ticking = true;
            }
        }

        private updateElementTransform() {

            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: string = value.join(" ");
            console.debug("transform = " + stringValue);
            this.viewerContent.style.transform = stringValue;
            (<any>this.viewerContent.style).webkitTransform = stringValue;
            (<any>this.viewerContent.style).MozTransform = stringValue;
            (<any>this.viewerContent.style).msTransform = stringValue;
            (<any>this.viewerContent.style).OTransform = stringValue;

            this.ticking = false;
        }
    }
}

【讨论】:

    【解决方案2】:

    使用变换时要注意的一点是应用它们的顺序。如果您切换scaletranslate,您会发现您的示例的工作方式大不相同。

    这是一篇关于这个问题的有趣文章:

    https://staff.washington.edu/fmf/2011/07/15/css3-transform-attribute-order/

    我无法修复您的版本,主要是因为当您切换转换顺序时它出现意外行为。基本上,您似乎遇到了奇怪的行为,因为比例本身会导致位置自动平移,然后您也会平移……而且这些不同的平移似乎以稍微不同的速度发生。

    不过,我确实重新实现了一个可行的版本,并允许您在缩放之前进行翻译。保持这个顺序的转换似乎可以避免这个问题。

    http://jsfiddle.net/fxpc5rao/32/

    我已将以下版本修改为使用translate3D,只是因为它在许多系统上表现更好。

    var current = {x: 0, y: 0, zoom: 1},
        con = document.getElementById('container');
        
    window.onclick = function(e) {
        var coef = e.shiftKey || e.ctrlKey ? 0.5 : 2,
            oz = current.zoom,
            nz = current.zoom * coef,
            /// offset of container
            ox = 20,
            oy = 20,
            /// mouse cords
            mx = e.clientX - ox,
            my = e.clientY - oy,
            /// calculate click at current zoom
            ix = (mx - current.x) / oz,
            iy = (my - current.y) / oz,
            /// calculate click at new zoom
            nx = ix * nz,
            ny = iy * nz,
            /// move to the difference
            /// make sure we take mouse pointer offset into account!
            cx = mx - nx,
            cy = my - ny
        ;
        // update current
        current.zoom = nz;
        current.x = cx;
        current.y = cy;
        /// make sure we translate before scale!
        con.style.transform
            = 'translate3D('+cx+'px, '+cy+'px,0) '
            + 'scale('+nz+')'
        ;
    };
    #container {
        position: absolute;
        left: 20px;
        top: 20px;
        width: 100%;
        height: 100%;
        transform-origin: 0 0 0;
        transition: transform 0.3s;
        transition-timing-function: ease-in-out;
        transform: translate3D(0,0,0) scale(1);
    }
    
    #item {
        position: absolute;
    }
    <div id="container">
        <div id="item">
            <img src="http://fadili.users.greyc.fr/demos/WaveRestore/EMInpaint/parrot_original.png" />
        </div>
    </div>

    更新

    我已经更新了我的答案(以及上面的 sn-p)以考虑到您的额外要求,您只需修改计算以包括鼠标指针偏移的差异。

    http://jsfiddle.net/fxpc5rao/33/

    现在每次点击都会添加计算的未缩放位置和e.clientX, e.clientY 之间的差异。这为您提供了保持鼠标指针周围发生缩放平移所需的偏移量。关键变化在这里:

    cx = (ix + (e.clientX - ix) - nx),
    cy = (iy + (e.clientY - iy) - ny)
    

    注意:因为您依赖于e.clientXe.clientY,如果您将#container 移离其当前的0,0 坐标,您会发现烦人的偏移。可以这样做,但您必须修改计算以将坐标定位到最终的 #container's 位置。

    更新 2

    很好的电话@Basj,我不知道转换是按相反的顺序发生的,我将在此处添加您评论中的链接:

    CSS3 transform order matters: rightmost operation first

    正如您所说,您需要在处理术语中的转换之前进行缩放,但在实际转换值中的缩放之前写入转换 - 如果这有意义:) 仍然不是完全正确 em> 确定为什么在另一个之前做一个会导致奇怪的插值。

    另外,我注意到有一个相当明显的优化——我敢肯定,当你实现它时,你会发现——没有必要添加一些东西只是为了稍后减去它。我想我那天只是过分了节日的欢呼!

    cx = e.clientX - nx,
    cy = e.clientY - ny
    

    更新 3

    没问题@jdavies,只需转换鼠标坐标,使其相对于容器的左上角。你如何计算这个偏移量将完全取决于你的项目(使用类似jQuery.offset 的东西来获得一个层的偏移量要容易得多——跨浏览器)。但是,我已更新此答案中的代码,以考虑使用绝对位置远离 0,0 的硬编码/固定偏移量 - 只是为了说明。这也是一个更新的小提琴:

    http://jsfiddle.net/fxpc5rao/5/

    当我们使用clientXclientY 时,鼠标坐标将始终从浏览器窗口的左上角开始计算,使它们对页面是全局的(disregarding scrolling)。为了将它们定位到容器中,您只需减去容器的 x 和 y 位置即可。

    Container at 0,0                Container at 80,100
    
    +------+------- screen x 0      +--------------- 0
    |      |                        |      
    |      |                        |  +------+
    |   x  | <-- mouse click        |  |x     | <-- mouse click
    +------+     at 100,120         |  |      |     at 100,120
    |                               |  |      |     but relative
    |                               |  +------+     20,20
    |                               |               so we us 20,20
    
    0 screen y                      0
    

    #container 也可以包含在其他元素中,您只需再次考虑这些元素赋予#container 的任何位置偏移量。在下面的小提琴中,有一个 #page-setting 元素用边距抵消一切,只要 ox, oy 变量用边距值更新,一切都应该表现。

    http://jsfiddle.net/fxpc5rao/34/

    注意:如果您将此系统放置在可滚动页面中,您还需要将viewport's scroll offsets 添加到鼠标坐标中,我在这里举了一个例子,但这很可能不是完整的跨浏览器解决方案。您最好查看像 jQuery 这样的已建立的库来为您计算坐标和偏移量。

    【讨论】:

    • 非常感谢@pebbl,问题解决了。我只能在 23 小时后给你赏金(现在如果我点击:不可能)。您的代码运行良好,只是您的解释中有一个小错误(也许您可以编辑?):不是:make sure we translate before scale!,而是make sure we scale before translate!,我现在 99% 确定感谢:stackoverflow.com/questions/27635272/… 第一个仿射变换(在数学意义)要使用transform: operation1 operation2 完成的是最右边的一个,即operation2
    • 我赞成这个,因为这个问题和答案绝对是坏蛋。干得好!
    • @pebbl - 非常感谢这个解决方案,它为我使用 jsPlumb 构建的工作流应用程序提供了无穷无尽的帮助。您是否可以详细说明当容器不在坐标 0,0 时发生的偏移量?我试图修改代码以修复此偏移量,但无济于事。我将不胜感激。再次感谢。
    • @jdavies ~ 没问题,很高兴它很有用 :) 我已经更新了我的答案,希望对您的问题有所帮助。
    • @pebbl - 非常感谢!这正是我所需要的。我已经对应用程序进行了更改,它现在可以正常运行。非常感谢您花时间解释这一点。
    猜你喜欢
    • 2014-12-16
    • 1970-01-01
    • 2011-12-09
    • 1970-01-01
    • 2014-06-25
    • 1970-01-01
    • 2012-08-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多