Skip to content

Instantly share code, notes, and snippets.

@iosonosempreio
Last active June 13, 2019 18:55
Show Gist options
  • Save iosonosempreio/1df5471d9046cc0a000f94d82e3e9614 to your computer and use it in GitHub Desktop.
Save iosonosempreio/1df5471d9046cc0a000f94d82e3e9614 to your computer and use it in GitHub Desktop.
Enable pinch-zoom, rotation and translation on HTML elements, with D3.js
function PinchElement(containerClass, options) {
this.options = undefined;
if (options){
this.options = options;
}
let container = d3.select(containerClass)
.style('overflow', 'hidden')
.style('position', 'relative');
let resizable = container.select('.resizable');
let touchPoint = container.selectAll('.touchPoint');
let info = {
firstDistance: 0,
firstAngle: 0,
firstPosition: {
x:0,
y:0
},
currentAngle:0,
currentScale: 1,
currentPosition: {
x:0,
y:0
},
totalScale: 1,
totalAngle:0,
totalPosition: {
x:0,
y:0
}
}
var tapedTwice = false;
let identifierP0;
container
.on('touchstart', function(){
d3.event.preventDefault();
let myTouches = d3.event.touches;
update(d3.event.touches);
let p0 = myTouches[0];
identifierP0 = p0.identifier;
info.firstPosition.x = p0.clientX;
info.firstPosition.y = p0.clientY;
if (myTouches.length > 1) {
let p1 = myTouches[1];
info.firstDistance = distance(p0.clientX, p0.clientY, p1.clientX, p1.clientY);
info.firstAngle = angle(p0.clientX, p0.clientY, p1.clientX, p1.clientY);
}
})
.on('touchmove', function(){
d3.event.preventDefault();
let myTouches = d3.event.touches;
move(d3.event.touches);
let p0 = myTouches[0];
if(p0.identifier != identifierP0) {
// means that the second finger is still on the screen while the first got detached
// to prevent unintended behaviour RETURN
return;
}
info.currentPosition.x = p0.clientX - info.firstPosition.x;
info.currentPosition.y = p0.clientY - info.firstPosition.y;
if (myTouches.length == 2) {
let p1 = myTouches[1];
info.currentScale = distance(p0.clientX, p0.clientY, p1.clientX, p1.clientY)/info.firstDistance;
if(options && options.maxScale) {
if(info.totalScale * info.currentScale > options.maxScale) {
info.currentScale = options.maxScale / info.totalScale
}
}
if (options && options.minScale) {
if(info.totalScale * info.currentScale < options.minScale) {
info.currentScale = options.minScale / info.totalScale
}
}
info.currentAngle = angle(p0.clientX, p0.clientY, p1.clientX, p1.clientY) - info.firstAngle;
resizable.style('transform',`
translate(${info.currentPosition.x + info.totalPosition.x}px, ${info.currentPosition.y + info.totalPosition.y}px)
scale(${info.totalScale * info.currentScale})
rotate(${info.totalAngle + info.currentAngle}deg)
`);
} else {
resizable.style('transform',`
translate(${info.currentPosition.x + info.totalPosition.x}px, ${info.currentPosition.y + info.totalPosition.y}px)
scale(${info.totalScale})
rotate(${info.totalAngle}deg)
`);
}
})
.on('touchend', function(){
d3.event.preventDefault();
let myTouches = d3.event.touches;
update(d3.event.touches);
if (myTouches.length == 0) {
info.totalPosition.x += info.currentPosition.x;
info.totalPosition.y += info.currentPosition.y;
if(!tapedTwice) {
tapedTwice = true;
setTimeout( function() { tapedTwice = false; }, 300 );
return false;
return;
}
// execute the following code if a double tap is detected;
reset()
}
if (myTouches.length == 1) {
info.totalScale *= info.currentScale;
info.totalAngle += info.currentAngle;
}
})
function update(data){
touchPoint = touchPoint.data(data, d => {return d.identifier} );
touchPoint.exit().remove()
touchPoint = touchPoint.enter().append('div')
.classed('touchPoint', true)
.style('left', d => { return d.clientX; })
.style('top', d => { return d.clientY; })
.style('width', 16)
.style('height', 16)
.style('display', 'block')
.style('border', '2px solid black')
.style('border-radius', '50%')
.style('position', 'absolute')
.merge(touchPoint);
}
function move(data){
touchPoint.data(data, d => {return d.identifier} )
.style('left', d => { return d.clientX;
})
.style('top', d => { return d.clientY; })
}
function distance(x1, y1, x2, y2) {
let a = x1 - x2;
let b = y1 - y2;
let c = Math.sqrt( a*a + b*b );
return c;
}
function angle(x1, y1, x2, y2) {
var angleDeg = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
return angleDeg;
}
let reset = function() {
info = {
firstDistance: 0,
firstAngle: 0,
firstPosition: {
x:0,
y:0
},
currentAngle:0,
currentScale: 1,
currentPosition: {
x:0,
y:0
},
totalScale: 1,
totalAngle:0,
totalPosition: {
x:0,
y:0
}
}
resizable.style('transform',`
translate(${info.currentPosition.x + info.totalPosition.x}px, ${info.currentPosition.y + info.totalPosition.y}px)
scale(${info.totalScale})
rotate(${info.totalAngle}deg)
`);
}
this.reset = function() {
return reset();
}
this.getTransformations = function() {
return {
translation: info.totalPosition,
rotation: info.totalAngle,
scale: info.totalScale
};
}
this.getOptions = function() {
return this.options;
}
}
function PinchElement(elementSelector, options) {
this.options = options;
let resizable = d3.select(elementSelector);
let container = resizable.select(function() { return this.parentNode; })
.style('overflow', 'hidden')
.style('position', 'relative');
let info = {
firstDistance: 0,
firstAngle: 0,
firstPosition: {
x:0,
y:0
},
currentAngle:0,
currentScale: 1,
currentPosition: {
x:0,
y:0
},
totalScale: 1,
totalAngle:0,
totalPosition: {
x:0,
y:0
}
}
let identifierP0;
container
.on('touchstart', function(){
d3.event.preventDefault();
let myTouches = d3.event.touches;
let p0 = myTouches[0];
identifierP0 = p0.identifier;
info.firstPosition.x = p0.clientX;
info.firstPosition.y = p0.clientY;
if (myTouches.length > 1) {
let p1 = myTouches[1];
info.firstDistance = distance(p0.clientX, p0.clientY, p1.clientX, p1.clientY);
info.firstAngle = angle(p0.clientX, p0.clientY, p1.clientX, p1.clientY);
}
if (options && options.onStart) {
options.onStart();
}
})
.on('touchmove', function(){
d3.event.preventDefault();
let myTouches = d3.event.touches;
let p0 = myTouches[0];
if(p0.identifier != identifierP0) {
// means that the second finger is still on the screen while the first got detached
// to prevent unintended behaviour RETURN
return;
}
info.currentPosition.x = p0.clientX - info.firstPosition.x;
info.currentPosition.y = p0.clientY - info.firstPosition.y;
if (myTouches.length == 2) {
let p1 = myTouches[1];
info.currentScale = distance(p0.clientX, p0.clientY, p1.clientX, p1.clientY)/info.firstDistance;
if(options && options.maxScale) {
if(info.totalScale * info.currentScale > options.maxScale) {
info.currentScale = options.maxScale / info.totalScale
}
}
if (options && options.minScale) {
if(info.totalScale * info.currentScale < options.minScale) {
info.currentScale = options.minScale / info.totalScale
}
}
info.currentAngle = angle(p0.clientX, p0.clientY, p1.clientX, p1.clientY) - info.firstAngle;
transform();
} else {
transform();
}
if (options && options.onTransform) {
options.onTransform();
}
})
.on('touchend', function(){
d3.event.preventDefault();
let myTouches = d3.event.touches;
if (myTouches.length == 0) {
info.totalPosition.x += info.currentPosition.x;
info.totalPosition.y += info.currentPosition.y;
}
if (myTouches.length == 1) {
info.totalScale *= info.currentScale;
info.totalAngle += info.currentAngle;
}
if (options && options.onEnd) {
options.onEnd();
}
})
function distance(x1, y1, x2, y2) {
let a = x1 - x2;
let b = y1 - y2;
let c = Math.sqrt( a*a + b*b );
return c;
}
function angle(x1, y1, x2, y2) {
var angleDeg = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
return angleDeg;
}
let transform = function(duration) {
resizable.transition()
.duration(duration)
.style('transform',`
translate(${info.currentPosition.x + info.totalPosition.x}px, ${info.currentPosition.y + info.totalPosition.y}px)
scale(${info.totalScale})
rotate(${info.totalAngle}deg)
`);
}
let getTransformation = function() {
return {
translationX: info.totalPosition.x,
translationY: info.totalPosition.y,
rotation: info.totalAngle,
scale: info.totalScale
};
}
let reset = function(duration) {
info = {
firstDistance: 0,
firstAngle: 0,
firstPosition: {
x: 0,
y: 0
},
currentAngle: 0,
currentScale: 1,
currentPosition: {
x: 0,
y: 0
},
totalScale: 1,
totalAngle: 0,
totalPosition: {
x: 0,
y: 0
}
}
transform(duration);
}
this.transform = function() {
return transform();
}
this.reset = function(duration) {
return reset(duration);
}
this.getTransformation = function() {
return getTransformation();
}
this.getOptions = function() {
return this.options;
}
this.getInfo = function() {
return info;
}
this.destroy = function(wantReset) {
if (wantReset) {
reset();
}
container
.style('overflow', '')
.style('position', '');
}
}
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Hello, world!</title>
<style>
body {
background-color: palegoldenrod;
padding: 0;
margin: 0;
}
.resize-container, .resize-container2 {
border: 1px solid slategray;
width: 90vw;
height: 90vw;
}
</style>
</head>
<body>
<h3>Works only on touch-screen devices</h3>
<div class="resize-container">
<img class="resizable1" src="https://cdn.stocksnap.io/img-thumbs/960w/IETQP9ZADV.jpg" style="width: 100%; height: auto;">
</div>
<div class="resize-container2">
<img class="resizable2" src="https://cdn.stocksnap.io/img-thumbs/960w/M2IXWNZDKT.jpg" style="width: 100%; height: auto;">
</div>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="d3-pinch-html-element.js"></script>
<script>
let options = {
minScale: 0.25,
maxScale: 5,
onTransform: function() { console.log(myPinchElement.getTransformation()) }
}
let myPinchElement = new PinchElement('.resizable1', null);
// let secondPinchElement = new PinchElement('.resize-container2')
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment