├── .gitignore
├── demo
└── index.html
├── LICENSE
├── README.md
├── style.css
└── photoTilt.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | *.iml
4 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
21 |
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 John Tregoning
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Photo Tilt
2 | =========
3 |
4 | HTML5 clone of the photo tilt feature/gesture/ux found in Facebook's Paper app.
5 |
6 | Basic Usage
7 | -----
8 | PhotoTilt (image url, container node)
9 | ```
10 | var photoTilt = new PhotoTilt({
11 | url: 'photo.jpg',
12 | lowResUrl: 'lowRes.jpg', //optional it will load lowRes 1st if available
13 | maxTilt: 18, //optional, defaults to 20
14 | container: document.body //optional, defaults to body
15 | reverseTilt: false //optional, defaults to false
16 | });
17 | ```
18 | Note: The speed of the tilt can be tweaked by updating the transform transtion speed in the CSS file.
19 |
20 | Demo
21 | ----
22 | You can find a working example [here](http://s3.jt.io/tilt/index.html) (make sure you test this on a device with a triaxial/accelerometer like a phone/tablet).
23 |
24 | More
25 | ----
26 | Blog post with extra information [here](http://jt.io/2014/photo-tilt/).
27 |
28 | TODO
29 | ----
30 |
31 | * add option for [full screen mode](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Using_full_screen_mode)
32 | * implement [lockOrientation](https://developer.mozilla.org/en-US/docs/Web/API/Screen.lockOrientation) (only works in FF)
33 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | overflow:hidden;
4 | height: 100%;
5 | width: 100%;
6 | padding: 0;
7 | margin: 0;
8 | background-color: #000;
9 | }
10 |
11 | .mask {
12 | position: absolute;
13 | top:0;
14 | left: 0;
15 | right: 0;
16 | bottom: 0;
17 | overflow: hidden;
18 | }
19 |
20 | .mask img {
21 | position: absolute;
22 | left: 0;
23 | top: 0;
24 | }
25 |
26 | .tilt-bar {
27 | position: fixed;
28 | bottom: 30px;
29 | left: 10px;
30 | right: 10px;
31 | height: 1px;
32 | z-index: 1;
33 | background-color: rgba(255,255,255,0.3);
34 | }
35 |
36 | .tilt-indicator {
37 | background-color: rgba(255,255,255,0.8);
38 | height: 1px;
39 | position: absolute;
40 | }
41 |
42 | .mask img,
43 | .tilt-indicator {
44 | -webkit-transition: -webkit-transform 0.2s linear;
45 | -moz-transition: -moz-transform 0.2s linear;
46 | -ms-transition: -ms-transform 0.2s linear;
47 | transition: transform 0.2s linear;
48 |
49 | -webkit-backface-visibility: hidden;
50 | -moz-backface-visibility: hidden;
51 | -ms-backface-visibility: hidden;
52 | backface-visibility: hidden;
53 |
54 | -webkit-perspective: 1000;
55 | -moz-perspective: 1000;
56 | -ms-perspective: 1000;
57 | perspective: 1000;
58 |
59 | -webkit-transform: translatez(0);
60 | -moz-transform: translatez(0);
61 | -ms-transform: translatez(0);
62 | transform: translatez(0);
63 | }
64 |
65 | .disable-transitions .mask img,
66 | .disable-transitions .tilt-indicator {
67 | -webkit-transition: none;
68 | -moz-transition: none;
69 | -ms-transition: none;
70 | transition: none;
71 | }
72 |
73 | .is-resizing {
74 | visibility: hidden;
75 | }
--------------------------------------------------------------------------------
/photoTilt.js:
--------------------------------------------------------------------------------
1 | var PhotoTilt = function(options) {
2 |
3 | 'use strict';
4 |
5 | var imgUrl = options.url,
6 | lowResUrl = options.lowResUrl,
7 | container = options.container || document.body,
8 | latestTilt = 0,
9 | timeoutID = 0,
10 | disableTilt,
11 | viewport,
12 | imgData,
13 | img,
14 | imgLoader,
15 | delta,
16 | centerOffset,
17 | tiltBarWidth,
18 | tiltCenterOffset,
19 | tiltBarIndicatorWidth,
20 | tiltBarIndicator,
21 | config;
22 |
23 | config = {
24 | maxTilt: options.maxTilt || 20,
25 | twoPhase: options.lowResUrl || false,
26 | reverseTilt: options.reverseTilt || false
27 | };
28 |
29 | window.requestAnimationFrame = window.requestAnimationFrame ||
30 | window.mozRequestAnimationFrame ||
31 | window.webkitRequestAnimationFrame ||
32 | window.msRequestAnimationFrame;
33 |
34 | var init = function() {
35 |
36 | var url = config.twoPhase ? lowResUrl : imgUrl;
37 |
38 | generateViewPort();
39 |
40 | preloadImg(url, function() {
41 |
42 | img = imgLoader.cloneNode(false);
43 | generateImgData();
44 | imgLoader = null;
45 |
46 | if (config.twoPhase) {
47 |
48 | preloadImg(imgUrl, function() {
49 | img.src = imgLoader.src;
50 | imgLoader = null;
51 | });
52 |
53 | }
54 |
55 | render();
56 | addEventListeners();
57 |
58 | });
59 |
60 | };
61 |
62 | var updatePosition = function() {
63 |
64 | var tilt = latestTilt,
65 | pxToMove;
66 |
67 | if (tilt > 0) {
68 | tilt = Math.min(tilt, config.maxTilt);
69 | } else {
70 | tilt = Math.max(tilt, config.maxTilt * -1);
71 | }
72 |
73 | if (!config.reverseTilt) {
74 | tilt = tilt * -1;
75 | }
76 |
77 | pxToMove = (tilt * centerOffset) / config.maxTilt;
78 |
79 | updateImgPosition(imgData, (centerOffset + pxToMove) * -1);
80 |
81 | updateTiltBar(tilt);
82 |
83 | window.requestAnimationFrame(updatePosition);
84 |
85 | };
86 |
87 | var updateTiltBar = function(tilt) {
88 |
89 | var pxToMove = (tilt * ((tiltBarWidth - tiltBarIndicatorWidth) / 2)) / config.maxTilt;
90 | setTranslateX(tiltBarIndicator, (tiltCenterOffset + pxToMove) );
91 |
92 | };
93 |
94 | var updateImgPosition = function(imgData, pxToMove) {
95 | setTranslateX(img, pxToMove);
96 | };
97 |
98 | var addEventListeners = function() {
99 |
100 | if (window.DeviceOrientationEvent) {
101 |
102 | var averageGamma = [];
103 |
104 | window.addEventListener('deviceorientation', function(eventData) {
105 |
106 | if (!disableTilt) {
107 |
108 | if (averageGamma.length > 8) {
109 | averageGamma.shift();
110 | }
111 |
112 | averageGamma.push(eventData.gamma);
113 |
114 | latestTilt = averageGamma.reduce(function(a, b) { return a+b; }) / averageGamma.length;
115 |
116 | }
117 |
118 | }, false);
119 |
120 | window.requestAnimationFrame(updatePosition);
121 |
122 | }
123 |
124 | window.addEventListener('resize', function() {
125 |
126 | container.classList.add('is-resizing');
127 | window.clearTimeout(timeoutID);
128 |
129 | timeoutID = window.setTimeout(function() {
130 |
131 | generateViewPort();
132 | container.innerHTML = "";
133 | render();
134 | container.classList.remove('is-resizing');
135 |
136 | }, 100);
137 |
138 | }, false);
139 |
140 | };
141 |
142 | var setTranslateX = function(node, amount) {
143 | node.style.webkitTransform =
144 | node.style.MozTransform =
145 | node.style.msTransform =
146 | node.style.transform = "translateX(" + Math.round(amount) + "px)";
147 | };
148 |
149 | var render = function() {
150 |
151 | var mask,
152 | tiltBar,
153 | resizedImgWidth,
154 | tiltBarPadding = 20;
155 |
156 | mask = document.createElement('div');
157 | mask.classList.add('mask');
158 |
159 | img.height = viewport.height;
160 | resizedImgWidth = (imgData.aspectRatio * img.height);
161 |
162 | delta = resizedImgWidth - viewport.width;
163 | centerOffset = delta / 2;
164 |
165 | tiltBar = document.createElement('div');
166 | tiltBar.classList.add('tilt-bar');
167 | tiltBarWidth = viewport.width - tiltBarPadding;
168 |
169 | tiltBarIndicator = document.createElement('div');
170 | tiltBarIndicator.classList.add('tilt-indicator');
171 |
172 | tiltBarIndicatorWidth = (viewport.width * tiltBarWidth) / resizedImgWidth;
173 | tiltBarIndicator.style.width = tiltBarIndicatorWidth + 'px';
174 |
175 | tiltCenterOffset = ((tiltBarWidth / 2) - (tiltBarIndicatorWidth / 2));
176 |
177 | updatePosition();
178 |
179 | if (tiltCenterOffset > 0) {
180 | disableTilt = false;
181 | tiltBar.appendChild(tiltBarIndicator);
182 | mask.appendChild(tiltBar);
183 | container.classList.remove('disable-transitions');
184 | } else {
185 | disableTilt = true;
186 | latestTilt = 0;
187 | container.classList.add('disable-transitions');
188 | }
189 |
190 | mask.appendChild(img);
191 | container.appendChild(mask);
192 |
193 | };
194 |
195 | var generateViewPort = function() {
196 |
197 | var containerStyle = window.getComputedStyle(container, null);
198 |
199 | viewport = {
200 | width: parseInt(containerStyle.width, 10),
201 | height: parseInt(containerStyle.height, 10)
202 | };
203 |
204 | };
205 |
206 | var generateImgData = function() {
207 |
208 | imgData = {
209 | width: imgLoader.width,
210 | height: imgLoader.height,
211 | aspectRatio: imgLoader.width / imgLoader.height,
212 | src: imgLoader.src
213 | };
214 |
215 | };
216 |
217 | var preloadImg = function(url, done) {
218 |
219 | imgLoader = new Image();
220 | imgLoader.addEventListener('load', done, false);
221 | imgLoader.src = url;
222 |
223 | };
224 |
225 | init();
226 |
227 | return {
228 | getContainer: function(){
229 | return container;
230 | }
231 | }
232 |
233 | };
--------------------------------------------------------------------------------