即使以下代码片段看起来很短,我也会在几天内挣扎(对我感到羞耻!)找到一种方法使用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>
&#13;
唯一的问题是转换是奇怪的,就像它首先翻译然后缩放一样;它会产生一种奇怪的锯齿形状。如何在这种情况下顺利进行CSS3转换?
在此处查看动画GIF 奇怪的过渡效果:http://gget.it/zf3fmwum/weirdtransition.gif
注意:点击的点是缩放变换的固定点(例如:单击眼睛,图像被缩放,光标仍然在眼睛上),就像在GoogleMaps-doubleclick-zoom中一样。
答案 0 :(得分:18)
使用转换时要注意的一件事是您应用它们的顺序。如果您切换scale
和translate
,您会发现您的示例工作方式有所不同。
这是一篇关于此事的有趣文章:
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+')'
;
};
&#13;
#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;
}
&#13;
<div id="container">
<div id="item">
<img src="http://fadili.users.greyc.fr/demos/WaveRestore/EMInpaint/parrot_original.png" />
</div>
</div>
&#13;
我已经更新了我的答案(以及上面的代码段)以考虑您的额外要求,您只需要修改计算以包含鼠标指针偏移的差异。
http://jsfiddle.net/fxpc5rao/33/
现在每次点击都会添加计算出的非缩放位置和e.clientX, e.clientY
之间的差异。这为您提供了在鼠标指针周围保持缩放平移所需的偏移量。关键变化在于:
cx = (ix + (e.clientX - ix) - nx),
cy = (iy + (e.clientY - iy) - ny)
注意:因为您依赖
e.clientX
和e.clientY
,如果您将#container
移离当前0,0
,您会发现令人讨厌的违规行为坐标。这可以完成,但您必须修改计算以将坐标本地化为最终的#container's
位置。
好的电话@Basj,我不知道转换是以相反的顺序发生的,我会在你的评论中添加链接:
CSS3 transform order matters: rightmost operation first
正如你所说,你需要在处理术语的翻译之前进行缩放,但是要在实际变换值中的缩放之前写入翻译 - 如果有意义的话:)仍然不是完全确定为什么在另一个之前做一个会导致奇数插值。
另外,我注意到有一个相当明显的优化 - 我确定,正如你实现的那样,你会发现 - 没有必要添加一些东西只是为了稍后减去它。我想那天我的节日欢呼太多了!
cx = e.clientX - nx,
cy = e.clientY - ny
没问题@jdavies,这只是转换鼠标坐标的问题,因此它们相对于容器的左上角。如何计算此偏移量将完全取决于您的项目(使用jQuery.offset之类的东西更容易获得图层的偏移 - 跨浏览器。但是我已经更新了这个答案中的代码,以便使用绝对位置考虑硬编码/固定偏移距离0,0 - 仅用于说明。这是一个更新的小提琴:
http://jsfiddle.net/fxpc5rao/5/
当我们使用clientX
和clientY
时,将始终从浏览器窗口的左上角计算鼠标坐标,使其成为页面的全局(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
元素可以使用margin来抵消所有内容,只要ox, oy
变量使用边缘值更新一切都应该表现。
http://jsfiddle.net/fxpc5rao/34/
注意:如果您将此系统放在可滚动页面中,您还需要将viewport's scroll offsets添加到鼠标坐标,我在这里举例说明,但这很可能不是完整的跨浏览器解决方案。你最好看一下像jQuery这样的已建立的库来为你计算坐标和偏移量。
答案 1 :(得分:1)
查看/缩放和缩小图像的问题很严重,对吧? :)
我终于成功校准了缩放算法,所以我想与社区分享。我创建了一个与底层图像交互的查看器类。我的解决方案中的一个重点是它不会修改默认的transform-origin,这对其他一些变换很有用。
你可以使用点击缩放/ ctrl +点击取消缩放,或者捏一下捏(使用Hammer JS)。警告,Firefox上默认不启用触摸事件。
对不起,我知道它使用的是Hammer和自制的Transform&amp; amp;点类,但请关注zoomTo方法,该方法与框架无关,是此缩放问题的主要内容。
(如果您愿意,可以在下面找到TypeScript版本)
在此代码段中试用
// 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;
}
}
}