Last active
June 13, 2019 18:55
-
-
Save iosonosempreio/1df5471d9046cc0a000f94d82e3e9614 to your computer and use it in GitHub Desktop.
Enable pinch-zoom, rotation and translation on HTML elements, with D3.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', ''); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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