diff --git a/bigbluebutton-html5/client/main.html b/bigbluebutton-html5/client/main.html
index 732f35244c3b09cc1d724c4d45fabc4e5f371d08..1cee0185f6857c851606ea9d67015362f3f6756f 100755
--- a/bigbluebutton-html5/client/main.html
+++ b/bigbluebutton-html5/client/main.html
@@ -42,6 +42,11 @@
       display: none !important;
     }
   </style>
+  <script>
+    document.addEventListener('gesturestart', function (e) {
+    e.preventDefault();
+});
+  </script>
 </head>
 <body style="background-color: #06172A">
   <div id="app" role="document"></div>
diff --git a/bigbluebutton-html5/imports/api/slides/server/methods.js b/bigbluebutton-html5/imports/api/slides/server/methods.js
index 5d69c6254dea71a8ca690ed6bf6ff926f44a4132..87212015815073ad8b0e9a5dbb49fe1f5ad1b9b5 100644
--- a/bigbluebutton-html5/imports/api/slides/server/methods.js
+++ b/bigbluebutton-html5/imports/api/slides/server/methods.js
@@ -1,7 +1,9 @@
 import { Meteor } from 'meteor/meteor';
 import mapToAcl from '/imports/startup/mapToAcl';
 import switchSlide from './methods/switchSlide';
+import zoomSlide from './methods/zoomSlide';
 
-Meteor.methods(mapToAcl(['methods.switchSlide'], {
+Meteor.methods(mapToAcl(['methods.switchSlide', 'methods.zoomSlide'], {
   switchSlide,
+  zoomSlide,
 }));
diff --git a/bigbluebutton-html5/imports/api/slides/server/methods/zoomSlide.js b/bigbluebutton-html5/imports/api/slides/server/methods/zoomSlide.js
new file mode 100644
index 0000000000000000000000000000000000000000..c546a88af87a3abf577c76a23a9ee7111ab48091
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/slides/server/methods/zoomSlide.js
@@ -0,0 +1,52 @@
+import Presentations from '/imports/api/presentations';
+import Slides from '/imports/api/slides';
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import RedisPubSub from '/imports/startup/server/redis';
+
+export default function switchSlide(credentials, slideNumber, podId, widthRatio, heightRatio, x, y) {
+  const REDIS_CONFIG = Meteor.settings.private.redis;
+
+  const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
+  const EVENT_NAME = 'ResizeAndMovePagePubMsg';
+
+  const { meetingId, requesterUserId, requesterToken } = credentials;
+
+  check(meetingId, String);
+  check(requesterUserId, String);
+  check(requesterToken, String);
+
+  const selector = {
+    meetingId,
+    podId,
+    current: true,
+  };
+  const Presentation = Presentations.findOne(selector);
+
+  if (!Presentation) {
+    throw new Meteor.Error('presentation-not-found', 'You need a presentation to be able to switch slides');
+  }
+
+  const Slide = Slides.findOne({
+    meetingId,
+    podId,
+    presentationId: Presentation.id,
+    num: slideNumber,
+  });
+
+  if (!Slide) {
+    throw new Meteor.Error('slide-not-found', `Slide number ${slideNumber} not found in the current presentation`);
+  }
+
+  const payload = {
+    podId,
+    presentationId: Presentation.id,
+    pageId: Slide.id,
+    xOffset: x,
+    yOffset: y,
+    widthRatio,
+    heightRatio,
+  };
+
+  return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
+}
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx
index e94cbe9105ce059d6d55cbff2162ea44dcaad9ed..505a377415ec69913395b9dbef68b6cea7b33435 100644
--- a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx
@@ -10,6 +10,8 @@ import PresentationOverlayContainer from './presentation-overlay/container';
 import Slide from './slide/component';
 import { styles } from './styles.scss';
 
+const HUNDRED_PERCENT = 100;
+const MAX_PERCENT = 400;
 
 export default class PresentationArea extends Component {
   constructor() {
@@ -19,9 +21,18 @@ export default class PresentationArea extends Component {
       presentationWidth: 0,
       presentationHeight: 0,
       showSlide: false,
+      zoom: 100,
+      touchZoom: false,
+      delta: {
+        x: 0,
+        y: 0,
+      },
     };
 
     this.getSvgRef = this.getSvgRef.bind(this);
+    this.zoomChanger = this.zoomChanger.bind(this);
+    this.touchUpdate = this.touchUpdate.bind(this);
+    this.pointUpdate = this.pointUpdate.bind(this);
   }
 
   componentDidMount() {
@@ -53,7 +64,7 @@ export default class PresentationArea extends Component {
       // By default presentation sizes are equal to the sizes of the refPresentationArea
       // direct parent of the svg wrapper
       let { clientWidth, clientHeight } = refPresentationArea;
-
+      
       // if a user is a presenter - this means there is a whiteboard toolbar on the right
       // and we have to get the width/height of the refWhiteboardArea
       // (inner hidden div with absolute position)
@@ -70,6 +81,7 @@ export default class PresentationArea extends Component {
   getInitialPresentationSizes() {
     // determining the presentationWidth and presentationHeight (available space for the svg)
     // on the initial load
+    
     const presentationSizes = this.getPresentationSizesAvailable();
     if (Object.keys(presentationSizes).length > 0) {
       // setting the state of the available space for the svg
@@ -123,7 +135,30 @@ export default class PresentationArea extends Component {
       height: adjustedHeight,
     };
   }
-
+  zoomChanger(zoom) {
+    let newZoom = zoom;
+    const isDifferent = newZoom !== this.state.zoom;
+
+    if (newZoom <= HUNDRED_PERCENT) {
+      newZoom = HUNDRED_PERCENT;
+    } else if (zoom >= MAX_PERCENT) {
+      newZoom = MAX_PERCENT;
+    }
+    if (isDifferent) this.setState({ zoom: newZoom });
+  }
+  pointUpdate(pointX, pointY) {
+    this.setState({
+      delta: {
+        x: pointX,
+        y: pointY,
+      },
+    });
+  }
+  touchUpdate(bool) {
+    this.setState({
+      touchZoom: bool,
+    });
+  }
   // renders the whole presentation area
   renderPresentationArea() {
     // sometimes tomcat publishes the slide url, but the actual file is not accessible (why?)
@@ -229,10 +264,21 @@ export default class PresentationArea extends Component {
 
     return (
       <PresentationOverlayContainer
+        podId={this.props.podId}
+        currentSlideNum={this.props.currentSlide.num}
+        slide={slideObj}
         whiteboardId={slideObj.id}
         slideWidth={width}
         slideHeight={height}
+        delta={this.state.delta}
+        viewBoxWidth={viewBoxWidth}
+        viewBoxHeight={viewBoxHeight}
+        zoom={this.state.zoom}
+        zoomChanger={this.zoomChanger}
+        adjustedSizes={adjustedSizes}
         getSvgRef={this.getSvgRef}
+        presentationSize={this.getPresentationSizesAvailable()}
+        touchZoom={this.state.touchZoom}
       >
         <WhiteboardOverlayContainer
           getSvgRef={this.getSvgRef}
@@ -241,10 +287,14 @@ export default class PresentationArea extends Component {
           slideHeight={height}
           viewBoxX={x}
           viewBoxY={y}
+          pointChanger={this.pointUpdate}
           viewBoxWidth={viewBoxWidth}
           viewBoxHeight={viewBoxHeight}
           physicalSlideWidth={(adjustedSizes.width / slideObj.widthRatio) * 100}
           physicalSlideHeight={(adjustedSizes.height / slideObj.heightRatio) * 100}
+          zoom={this.state.zoom}
+          zoomChanger={this.zoomChanger}
+          touchUpdate={this.touchUpdate}
         />
       </PresentationOverlayContainer>
     );
@@ -260,6 +310,8 @@ export default class PresentationArea extends Component {
         podId={this.props.podId}
         currentSlideNum={this.props.currentSlide.num}
         presentationId={this.props.currentSlide.presentationId}
+        zoom={this.state.zoom}
+        zoomChanger={this.zoomChanger}
       />
     );
   }
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/component.jsx
index 176cb950a8972c0cdafbefd9ce5e3e410a2d0458..f55c1c94b37d098b7b43679ecc9d0e3e6e4829d4 100755
--- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/component.jsx
@@ -1,7 +1,13 @@
 import React, { Component } from 'react';
 import PropTypes from 'prop-types';
-
+import SlideCalcUtil from '/imports/utils/slideCalcUtils';
+// After lots of trial and error on why synching doesn't work properly, I found I had to
+// multiply the coordinates by 2. There's something I don't understand probably on the
+// canvas coordinate system. (ralam feb 22, 2012)
+const MYSTERY_NUM = 2;
 const CURSOR_INTERVAL = 16;
+const HUNDRED_PERCENT = 100;
+const MAX_PERCENT = 400;
 
 export default class PresentationOverlay extends Component {
   constructor(props) {
@@ -17,6 +23,9 @@ export default class PresentationOverlay extends Component {
 
     // id of the setInterval()
     this.intervalId = 0;
+    this.state = {
+      zoom: props.zoom,
+    };
 
     // Mobile Firefox has a bug where e.preventDefault on touchstart doesn't prevent
     // onmousedown from triggering right after. Thus we have to track it manually.
@@ -35,20 +44,199 @@ export default class PresentationOverlay extends Component {
     this.mouseOutHandler = this.mouseOutHandler.bind(this);
     this.getTransformedSvgPoint = this.getTransformedSvgPoint.bind(this);
     this.svgCoordinateToPercentages = this.svgCoordinateToPercentages.bind(this);
+    this.mouseZoomHandler = this.mouseZoomHandler.bind(this);
+    this.zoomCalculation = this.zoomCalculation.bind(this);
+    this.doZoomCall = this.doZoomCall.bind(this);
+    this.tapHandler = this.tapHandler.bind(this);
+    this.zoomCall = this.zoomCall.bind(this);
+
+    this.panZoom = this.panZoom.bind(this);
+    this.pinchZoom = this.pinchZoom.bind(this);
+    this.toolbarZoom = this.toolbarZoom.bind(this);
+
+    this.isPortraitDoc = this.isPortraitDoc.bind(this);
+    this.doWidthBoundsDetection = this.doWidthBoundsDetection.bind(this);
+    this.doHeightBoundsDetection = this.doHeightBoundsDetection.bind(this);
+    this.calcViewedRegion = this.calcViewedRegion.bind(this);
+    this.onZoom = this.onZoom.bind(this);
+
+    const {
+      viewBoxWidth,
+      viewBoxHeight,
+      slideWidth,
+      slideHeight,
+      slide,
+    } = props;
+
+    this.fitToPage = false;
+
+    this.viewportW = slideWidth;
+    this.viewportH = slideHeight;
+
+    this.viewedRegionX = slide.xOffset;
+    this.viewedRegionY = slide.yOffset;
+    this.viewedRegionW = (viewBoxWidth / slideWidth) * 100;
+    this.viewedRegionH = (viewBoxHeight / slideHeight) * 100;
+
+    this.pageOrigW = slideWidth;
+    this.pageOrigH = slideHeight;
+
+    this.calcPageW = this.pageOrigW / (this.viewedRegionW / HUNDRED_PERCENT);
+    this.calcPageH = this.pageOrigH / (this.viewedRegionH / HUNDRED_PERCENT);
+    this.calcPageX = (this.viewedRegionX / HUNDRED_PERCENT) * this.calcPageW;
+    this.calcPageY = (this.viewedRegionY / HUNDRED_PERCENT) * this.calcPageH;
+
+    this.tapedTwice = false;
+    this.touches = [];
   }
-  // transforms the coordinate from window coordinate system
-  // to the main svg coordinate system
+
+  componentDidMount() {
+    const {
+      viewBoxWidth,
+      slideWidth,
+      zoomChanger,
+    } = this.props;
+
+    const realZoom = (viewBoxWidth / slideWidth) * 100;
+
+    const zoomPercentage = (Math.round((100 / realZoom) * 100));
+    const roundedUpToFive = Math.round(zoomPercentage / 5) * 5;
+    zoomChanger(roundedUpToFive);
+  }
+
+  componentDidUpdate(prevProps) {
+    const {
+      zoom,
+      delta,
+      touchZoom,
+    } = this.props;
+    const isDifferent = zoom !== this.state.zoom && !touchZoom;
+    const moveSLide = ((delta.x !== prevProps.delta.x)
+    || (delta.y !== prevProps.delta.y)) && !isDifferent;
+    const isTouchZoom = zoom !== this.state.zoom && touchZoom;
+    if (moveSLide) {
+      this.panZoom();
+    }
+
+    if (isTouchZoom) {
+      this.pinchZoom();
+    }
+
+    if (isDifferent) {
+      this.toolbarZoom();
+    }
+  }
+
+  onZoom(zoomValue, mouseX, mouseY) {
+    let absXcoordInPage = (Math.abs(this.calcPageX) * MYSTERY_NUM) + mouseX;
+    let absYcoordInPage = (Math.abs(this.calcPageY) * MYSTERY_NUM) + mouseY;
+
+    const relXcoordInPage = absXcoordInPage / this.calcPageW;
+    const relYcoordInPage = absYcoordInPage / this.calcPageH;
+
+    if (this.isPortraitDoc() && this.fitToPage) {            
+      this.calcPageH = (this.viewportH * zoomValue) / HUNDRED_PERCENT;
+      this.calcPageW = (this.pageOrigW / this.pageOrigH) * this.calcPageH;
+    } else if (!this.isPortraitDoc() && this.fitToPage) {      
+      this.calcPageW = (this.viewportW * zoomValue) / HUNDRED_PERCENT;
+      this.calcPageH = (this.viewportH * zoomValue) / HUNDRED_PERCENT;
+    } else {
+      this.calcPageW = (this.viewportW * zoomValue) / HUNDRED_PERCENT;
+      this.calcPageH = (this.calcPageW / this.pageOrigW) * this.pageOrigH;
+    }
+
+    absXcoordInPage = relXcoordInPage * this.calcPageW;
+    absYcoordInPage = relYcoordInPage * this.calcPageH;
+
+    this.calcPageX = -((absXcoordInPage - mouseX) / MYSTERY_NUM);
+    this.calcPageY = -((absYcoordInPage - mouseY) / MYSTERY_NUM);
+
+    this.doWidthBoundsDetection();
+    this.doHeightBoundsDetection();
+
+    this.calcViewedRegion();
+  }
+
   getTransformedSvgPoint(clientX, clientY) {
     const svgObject = this.props.getSvgRef();
     const screenPoint = svgObject.createSVGPoint();
     screenPoint.x = clientX;
     screenPoint.y = clientY;
-
     // transform a screen point to svg point
     const CTM = svgObject.getScreenCTM();
+
     return screenPoint.matrixTransform(CTM.inverse());
   }
 
+  panZoom() {
+    const {
+      delta,
+    } = this.props;
+    this.deltaX = delta.x;
+    this.deltaY = delta.y;
+    this.calcPageX += delta.x * -1;
+    this.calcPageY += delta.y * -1;
+    this.doHeightBoundsDetection();
+    this.doWidthBoundsDetection();
+    this.calcViewedRegion();
+    this.zoomCall(
+      this.state.zoom,
+      this.viewedRegionW,
+      this.viewedRegionH,
+      this.viewedRegionX,
+      this.viewedRegionY,
+    );
+  }
+
+  pinchZoom() {
+    const {
+      zoom,
+    } = this.props;
+    const posX = this.touches[0].clientX;
+    const posY = this.touches[0].clientY;
+    this.doZoomCall(zoom, posX, posY);
+  }
+
+  toolbarZoom() {
+    const {
+      getSvgRef,
+      zoom,
+    } = this.props;
+    const svgRect = getSvgRef().getBoundingClientRect();
+    const svgCenterX = svgRect.left + (svgRect.width / 2);
+    const svgCenterY = svgRect.top + (svgRect.height / 2);
+    this.doZoomCall(zoom, svgCenterX, svgCenterY);
+  }
+
+  isPortraitDoc() {
+    return this.pageOrigH > this.pageOrigW;
+  }
+
+  doWidthBoundsDetection() {
+    const verifyPositionToBound = (this.calcPageW + (this.calcPageX * MYSTERY_NUM));
+    if (this.calcPageX >= 0) {
+      this.calcPageX = 0;
+    } else if (verifyPositionToBound < this.viewportW) {
+      this.calcPageX = (this.viewportW - this.calcPageW) / MYSTERY_NUM;
+    }
+  }
+
+  doHeightBoundsDetection() {
+    const verifyPositionToBound = (this.calcPageH + (this.calcPageY * MYSTERY_NUM));
+    if (this.calcPageY >= 0) {
+      this.calcPageY = 0;
+    } else if (verifyPositionToBound < this.viewportH) {
+      this.calcPageY = (this.viewportH - this.calcPageH) / MYSTERY_NUM;
+    }
+  }
+
+  calcViewedRegion() {
+    this.viewedRegionW = SlideCalcUtil.calcViewedRegionWidth(this.viewportW, this.calcPageW);
+    this.viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(this.viewportH, this.calcPageH);
+    this.viewedRegionX = SlideCalcUtil.calcViewedRegionX(this.calcPageX, this.calcPageW);
+    this.viewedRegionY = SlideCalcUtil.calcViewedRegionY(this.calcPageY, this.calcPageH);
+  }
+
   checkCursor() {
     // check if the cursor hasn't moved since last check
     if (this.lastSentClientX !== this.currentClientX
@@ -56,6 +244,7 @@ export default class PresentationOverlay extends Component {
       const { currentClientX, currentClientY } = this;
       // retrieving a transformed coordinate
       let transformedSvgPoint = this.getTransformedSvgPoint(currentClientX, currentClientY);
+
       // determining the cursor's coordinates as percentages from the slide's width/height
       transformedSvgPoint = this.svgCoordinateToPercentages(transformedSvgPoint);
       // updating last sent raw coordinates
@@ -81,11 +270,93 @@ export default class PresentationOverlay extends Component {
     return point;
   }
 
+  zoomCalculation(zoom, mouseX, mouseY) {
+    const svgPosition = this.getTransformedSvgPoint(mouseX, mouseY);
+    this.onZoom(zoom, svgPosition.x, svgPosition.y);
 
-  handleTouchStart(event) {
-    // to prevent default behavior (scrolling) on devices (in Safari), when you draw a text box
+    return {
+      viewedRegionW: this.viewedRegionW,
+      viewedRegionH: this.viewedRegionH,
+      viewedRegionX: this.viewedRegionX,
+      viewedRegionY: this.viewedRegionY,
+    };
+  }
+
+  zoomCall(zoom, w, h, x, y) {
+    const {
+      zoomSlide,
+      podId,
+      currentSlideNum,
+    } = this.props;
+    zoomSlide(currentSlideNum, podId, w, h, x, y);
+    this.setState({ zoom });
+  }
+
+  doZoomCall(zoom, mouseX, mouseY) {
+    const zoomData = this.zoomCalculation(zoom, mouseX, mouseY);
+
+    const {
+      viewedRegionW,
+      viewedRegionH,
+      viewedRegionX,
+      viewedRegionY,
+    } = zoomData;
+
+    this.zoomCall(zoom, viewedRegionW, viewedRegionH, viewedRegionX, viewedRegionY);
+    this.props.zoomChanger(zoom);
+  }
+
+  mouseZoomHandler(e) {
+    const {
+      zoom,
+      whiteboardId,
+      updateCursor,
+    } = this.props;
+
+    let newZoom = zoom;
+    if (e.deltaY < 0) {
+      newZoom += 5;
+    }
+    if (e.deltaY > 0) {
+      newZoom -= 5;
+    }
+    if (newZoom <= HUNDRED_PERCENT) {
+      newZoom = HUNDRED_PERCENT;
+    } else if (newZoom >= MAX_PERCENT) {
+      newZoom = MAX_PERCENT;
+    } 
+
+    const mouseX = e.clientX;
+    const mouseY = e.clientY;
+    const svgPosition = this.getTransformedSvgPoint(mouseX, mouseY);
+    const svgPercentage = this.svgCoordinateToPercentages(svgPosition);
+
+    this.doZoomCall(newZoom, mouseX, mouseY);
+    updateCursor({
+      xPercent: svgPercentage.x,
+      yPercent: svgPercentage.y,
+      whiteboardId,
+    });
+  }
+  tapHandler(event) {
+    if (event.touches.length === 2) return; 
+    if (!this.tapedTwice) {
+      this.tapedTwice = true;
+      setTimeout(() => this.tapedTwice = false, 300);
+      return;
+    }
     event.preventDefault();
+    const sizeDefault = this.state.zoom === HUNDRED_PERCENT;
+    
+    if (sizeDefault) {      
+      this.doZoomCall(200, this.currentClientX, this.currentClientY);
+      return;
+    }
+    this.doZoomCall(HUNDRED_PERCENT, 0, 0);
+ }
 
+  handleTouchStart(event) {
+    // to prevent default behavior (scrolling) on devices (in Safari), when you draw a text box
     window.addEventListener('touchend', this.handleTouchEnd, { passive: false });
     window.addEventListener('touchmove', this.handleTouchMove, { passive: false });
     window.addEventListener('touchcancel', this.handleTouchCancel, true);
@@ -95,9 +366,13 @@ export default class PresentationOverlay extends Component {
     const { clientX, clientY } = event.changedTouches[0];
     this.currentClientX = clientX;
     this.currentClientY = clientY;
+    if (event.touches.length === 2) {
+      this.touches = [...event.touches];
+    }
 
     const intervalId = setInterval(this.checkCursor, CURSOR_INTERVAL);
     this.intervalId = intervalId;
+    this.tapHandler(event);
   }
 
   handleTouchMove(event) {
@@ -111,7 +386,7 @@ export default class PresentationOverlay extends Component {
 
   handleTouchEnd(event) {
     event.preventDefault();
-
+    
     // touch ended, removing the interval
     clearInterval(this.intervalId);
     this.intervalId = 0;
@@ -153,7 +428,6 @@ export default class PresentationOverlay extends Component {
     if (this.touchStarted) {
       return;
     }
-
     // for the case where you change settings in one of the lists (which are displayed on the slide)
     // the mouse starts pointing to the slide right away and mouseEnter doesn't fire
     // so we call it manually here
@@ -201,6 +475,8 @@ export default class PresentationOverlay extends Component {
           onMouseOut={this.mouseOutHandler}
           onMouseEnter={this.mouseEnterHandler}
           onMouseMove={this.mouseMoveHandler}
+          onWheel={this.mouseZoomHandler}
+          onBlur={() => {}}
           style={{ width: '100%', height: '100%', touchAction: 'none' }}
         >
           {this.props.children}
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/container.jsx
index 89f5e39c6f79db9ed7cca302a0edb8b629de28ac..6665175b27f7fc90446e68195f5a6a973fdaea27 100644
--- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/container.jsx
@@ -2,6 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTracker } from 'meteor/react-meteor-data';
 import PresentationOverlayService from './service';
+import PresentationToolbarService from '../presentation-toolbar/service';
 import PresentationOverlay from './component';
 
 const PresentationOverlayContainer = ({ children, ...rest }) => (
@@ -10,8 +11,12 @@ const PresentationOverlayContainer = ({ children, ...rest }) => (
   </PresentationOverlay>
 );
 
-export default withTracker(() => ({
+export default withTracker(({ podId, currentSlideNum, slide }) => ({
+  slide,
+  podId,
+  currentSlideNum,
   updateCursor: PresentationOverlayService.updateCursor,
+  zoomSlide: PresentationToolbarService.zoomSlide,
 }))(PresentationOverlayContainer);
 
 PresentationOverlayContainer.propTypes = {
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx
index 170b194e34ada802b9880e62bf11f4c7a29f39f0..1cf9ff0d54f0a694464728aff7e4b65d81d4aaf7 100644
--- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx
@@ -1,9 +1,15 @@
 import React, { Component } from 'react';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import browser from 'browser-detect';
 import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
 import Button from '/imports/ui/components/button/component';
 import { styles } from './styles.scss';
+import ZoomTool from './zoom-tool/component';
+
+const STEP = 5;
+const HUNDRED_PERCENT = 100;
+const MAX_PERCENT = 400;
 
 const intlMessages = defineMessages({
   previousSlideLabel: {
@@ -125,6 +131,8 @@ class PresentationToolbar extends Component {
     this.state = { sliderValue: 100 };
     this.handleValuesChange = this.handleValuesChange.bind(this);
     this.handleSkipToSlideChange = this.handleSkipToSlideChange.bind(this);
+    this.change = this.change.bind(this);
+    this.setInt = 0;
   }
 
   handleSkipToSlideChange(event) {
@@ -133,7 +141,10 @@ class PresentationToolbar extends Component {
   }
 
   handleValuesChange(event) {
-    this.setState({ sliderValue: event.target.value });
+    this.setState(
+      { sliderValue: event.target.value },
+      () => this.handleZoom(this.state.sliderValue),
+    );
   }
 
   fitToWidthClickHandler() {
@@ -147,6 +158,11 @@ class PresentationToolbar extends Component {
       fitToScreenValue: 'not_implemented_yet',
     });
   }
+
+  change(value) {
+    this.props.zoomChanger(value);
+  }
+
   renderSkipSlideOpts(numberOfSlides) {
     // Fill drop down menu with all the slides in presentation
     const { intl } = this.props;
@@ -172,51 +188,71 @@ class PresentationToolbar extends Component {
       numberOfSlides,
       actions,
       intl,
+      zoom,
     } = this.props;
 
+    const BROWSER_RESULTS = browser();
+    const isMobileBrowser = BROWSER_RESULTS.mobile ||
+      BROWSER_RESULTS.os.includes('Android');
     return (
       <div id="presentationToolbarWrapper" className={styles.presentationToolbarWrapper}>
         {PresentationToolbar.renderAriaLabelsDescs()}
-        <Button
-          role="button"
-          aria-labelledby="prevSlideLabel"
-          aria-describedby="prevSlideDesc"
-          disabled={!(currentSlideNum > 1)}
-          color="default"
-          icon="left_arrow"
-          size="md"
-          onClick={actions.previousSlideHandler}
-          label={intl.formatMessage(intlMessages.previousSlideLabel)}
-          hideLabel
-          className={styles.prevSlide}
-        />
-        <select
-          // <select> has an implicit role of listbox, no need to define role="listbox" explicitly
-          id="skipSlide"
-          aria-labelledby="skipSlideLabel"
-          aria-describedby="skipSlideDesc"
-          aria-live="polite"
-          aria-relevant="all"
-          value={currentSlideNum}
-          onChange={actions.skipToSlideHandler}
-          className={styles.skipSlideSelect}
-        >
-          {this.renderSkipSlideOpts(numberOfSlides)}
-        </select>
-        <Button
-          role="button"
-          aria-labelledby="nextSlideLabel"
-          aria-describedby="nextSlideDesc"
-          disabled={!(currentSlideNum < numberOfSlides)}
-          color="default"
-          icon="right_arrow"
-          size="md"
-          onClick={actions.nextSlideHandler}
-          label={intl.formatMessage(intlMessages.nextSlideLabel)}
-          hideLabel
-          className={styles.skipSlide}
-        />
-
+        {
+          <span className={styles.presentationControls}>
+            <Button
+              role="button"
+              aria-labelledby="prevSlideLabel"
+              aria-describedby="prevSlideDesc"
+              disabled={!(currentSlideNum > 1)}
+              color="default"
+              icon="left_arrow"
+              size="md"
+              onClick={actions.previousSlideHandler}
+              label={intl.formatMessage(intlMessages.previousSlideLabel)}
+              hideLabel
+              className={styles.prevSlide}
+            />
+            <select
+              // <select> has an implicit role of listbox, no need to define role="listbox" explicitly
+              id="skipSlide"
+              aria-labelledby="skipSlideLabel"
+              aria-describedby="skipSlideDesc"
+              aria-live="polite"
+              aria-relevant="all"
+              value={currentSlideNum}
+              onChange={actions.skipToSlideHandler}
+              className={styles.skipSlideSelect}
+            >
+              {this.renderSkipSlideOpts(numberOfSlides)}
+            </select>
+            <Button
+              role="button"
+              aria-labelledby="nextSlideLabel"
+              aria-describedby="nextSlideDesc"
+              disabled={!(currentSlideNum < numberOfSlides)}
+              color="default"
+              icon="right_arrow"
+              size="md"
+              onClick={actions.nextSlideHandler}
+              label={intl.formatMessage(intlMessages.nextSlideLabel)}
+              hideLabel
+              className={styles.skipSlide}
+            />
+          </span>
+        }
+        {
+          !isMobileBrowser ?
+            <span className={styles.zoomWrapper}>
+              <ZoomTool
+                value={zoom}
+                change={this.change}
+                minBound={HUNDRED_PERCENT}
+                maxBound={MAX_PERCENT}
+                step={STEP}
+              />
+            </span>
+            : null
+        }
         {/* Fit to width button
         <Button
           role="button"
@@ -243,29 +279,6 @@ class PresentationToolbar extends Component {
           label={'Fit to Screen'}
           hideLabel={true}
         /> */}
-        {/* Zoom slider
-        <div
-          className={classNames(styles.zoomWrapper, { [styles.zoomWrapperNoBorder]: true })}
-        >
-          <div className={styles.zoomMinMax}> 100% </div>
-          <input
-            role="slider"
-            aria-labelledby="zoomLabel"
-            aria-describedby="zoomDesc"
-            aria-valuemax="400"
-            aria-valuemin="100"
-            aria-valuenow={this.state.sliderValue}
-            value={this.state.sliderValue}
-            step="5"
-            type="range"
-            min="100"
-            max="400"
-            onChange={this.handleValuesChange}
-            onInput={this.handleValuesChange}
-            className={styles.zoomSlider}
-          />
-          <div className={styles.zoomMinMax}> 400% </div>
-        </div> */}
       </div>
     );
   }
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx
index da6fd3d779d0d05c0aac31352d6768c75ffa6b6e..66767f2f5562d38d22ade1647794169e19b34e51 100644
--- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx
@@ -1,10 +1,10 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withTracker } from 'meteor/react-meteor-data';
+import PresentationService from '/imports/ui/components/presentation/service';
 import PresentationToolbarService from './service';
 import PresentationToolbar from './component';
 
-import PresentationService from '/imports/ui/components/presentation/service';
 
 const PresentationToolbarContainer = (props) => {
   const {
@@ -12,15 +12,21 @@ const PresentationToolbarContainer = (props) => {
     userIsPresenter,
     numberOfSlides,
     actions,
+    zoom,
+    zoomChanger,
   } = props;
 
   if (userIsPresenter) {
     // Only show controls if user is presenter
     return (
       <PresentationToolbar
-        currentSlideNum={currentSlideNum}
-        numberOfSlides={numberOfSlides}
-        actions={actions}
+        {...{
+          currentSlideNum,
+          numberOfSlides,
+          actions,
+          zoom,
+          zoomChanger,
+        }}
       />
     );
   }
@@ -38,6 +44,8 @@ export default withTracker((params) => {
   return {
     userIsPresenter: PresentationService.isPresenter(podId),
     numberOfSlides,
+    zoom: params.zoom,
+    zoomChanger: params.zoomChanger,
     actions: {
       nextSlideHandler: () =>
         PresentationToolbarService.nextSlide(params.currentSlideNum, numberOfSlides, podId),
@@ -45,6 +53,8 @@ export default withTracker((params) => {
         PresentationToolbarService.previousSlide(params.currentSlideNum, podId),
       skipToSlideHandler: requestedSlideNum =>
         PresentationToolbarService.skipToSlide(requestedSlideNum, podId),
+      zoomSlideHandler: value =>
+        PresentationToolbarService.zoomSlide(params.currentSlideNum, podId, value),
     },
   };
 })(PresentationToolbarContainer);
@@ -52,6 +62,8 @@ export default withTracker((params) => {
 PresentationToolbarContainer.propTypes = {
   // Number of current slide being displayed
   currentSlideNum: PropTypes.number.isRequired,
+  zoom: PropTypes.number.isRequired,
+  zoomChanger: PropTypes.func.isRequired,
 
   // Is the user a presenter
   userIsPresenter: PropTypes.bool.isRequired,
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js
index 2b16bd03c50fdebd45884f41e9ddaa78a17b94ec..a9c98a1c1ea011ea430ce08fc74f1b263a258208 100644
--- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js
+++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js
@@ -40,6 +40,11 @@ const nextSlide = (currentSlideNum, numberOfSlides, podId) => {
   }
 };
 
+const zoomSlide = (currentSlideNum, podId, widthRatio, heightRatio, xOffset, yOffset) => {
+  makeCall('zoomSlide', currentSlideNum, podId, widthRatio, heightRatio, xOffset, yOffset);
+};
+
+
 const skipToSlide = (requestedSlideNum, podId) => {
   makeCall('switchSlide', requestedSlideNum, podId);
 };
@@ -49,4 +54,5 @@ export default {
   nextSlide,
   previousSlide,
   skipToSlide,
+  zoomSlide,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss
index 63b7cadc87464cc58a02465a02e1429fd0f99b09..b5ba291587c5c9be3bc9c37b6863bc870271521e 100644
--- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss
@@ -6,18 +6,25 @@ $controls-background: $color-white !default;
 $toolbar-button-border-radius: 5px;
 
 .presentationToolbarWrapper,
+.presentationControls,
 .zoomWrapper {
   order: 2;
   display: flex;
   flex-direction: row;
   align-items: center;
+  margin-left: .5rem;
+  margin-right: .5rem;
+  border-radius: $toolbar-button-border-radius;
+}
+
+.presentationControls,
+.zoomWrapper {
+  box-shadow: 0 0 10px -2px rgba(0, 0, 0, .25);
 }
 
 .presentationToolbarWrapper {
   position: absolute;
   bottom: .8rem;
-  box-shadow: 0 0 10px -2px rgba(0, 0, 0, .25);
-  border-radius: $toolbar-button-border-radius;
   align-self: center;
   justify-content: center;
   z-index: 1;
@@ -27,6 +34,7 @@ $toolbar-button-border-radius: 5px;
     transform-origin: bottom;
   }
 
+
   button,
   select,
   > div {
@@ -36,7 +44,6 @@ $toolbar-button-border-radius: 5px;
     border-bottom: 0;
     border-left: 0;
     border-radius: 0;
-    box-shadow: none;
     height: 2.25rem;
     box-shadow: none !important;
     border: 0;
@@ -61,10 +68,12 @@ $toolbar-button-border-radius: 5px;
 }
 
 .zoomWrapper {
-  border-radius: 0 5px 5px 0;
+  // border-radius: 0 5px 5px 0;
   justify-content: space-between;
+  // flex-direction: column;
   width: 11.5%;
   min-width: 175px;
+  background-color: $color-white;
 }
 
 .zoomWrapperNoBorder {
@@ -95,3 +104,14 @@ $toolbar-button-border-radius: 5px;
 .zoomMinMax {
   font-weight: normal;
 }
+.zoomPercentageDisplay {
+  width: 100%;
+  height: 2.25rem;
+  text-align: center;
+  color: black;
+  border-left: $border-size solid $color-gray-lighter !important;
+  border-right: $border-size solid $color-gray-lighter !important;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
\ No newline at end of file
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/zoom-tool/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/zoom-tool/component.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..0e976e08d004a7f378fde855070ecf1ec1d3b475
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/zoom-tool/component.jsx
@@ -0,0 +1,206 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+import Button from '/imports/ui/components/button/component';
+import { styles } from '../styles.scss';
+
+const DELAY_MILLISECONDS = 200;
+const STEP_TIME = 100;
+
+export default class ZoomTool extends Component {
+  static renderAriaLabelsDescs() {
+    return (
+      <div hidden key="hidden-div">
+        {/* Zoom in button aria */}
+        <div id="zoomInLabel">
+          <FormattedMessage
+            id="app.presentation.presentationToolbar.zoomInLabel"
+            description="Aria label for when switching to previous slide"
+            defaultMessage="Previous slide"
+          />
+        </div>
+        <div id="zoomInDesc">
+          <FormattedMessage
+            id="app.presentation.presentationToolbar.zoomInDesc"
+            description="Aria description for when switching to previous slide"
+            defaultMessage="Change the presentation to the previous slide"
+          />
+        </div>
+        {/* Zoom out button aria */}
+        <div id="zoomOutLabel">
+          <FormattedMessage
+            id="app.presentation.presentationToolbar.zoomOutLabel"
+            description="Aria label for when switching to next slide"
+            defaultMessage="Next slide"
+          />
+        </div>
+        <div id="zoomOutDesc">
+          <FormattedMessage
+            id="app.presentation.presentationToolbar.zoomOutDesc"
+            description="Aria description for when switching to next slide"
+            defaultMessage="Change the presentation to the next slide"
+          />
+        </div>
+        {/* Zoom indicator aria */}
+        <div id="zoomIndicator">
+          <FormattedMessage
+            id="app.presentation.presentationToolbar.zoomIndicator"
+            description="Aria label for when switching to a specific slide"
+            defaultMessage="Skip slide"
+          />
+        </div>
+      </div>
+    );
+  }
+  constructor(props) {
+    super(props);
+    this.increment = this.increment.bind(this);
+    this.decrement = this.decrement.bind(this);
+    this.mouseDownHandler = this.mouseDownHandler.bind(this);
+    this.mouseUpHandler = this.mouseUpHandler.bind(this);
+    this.execInterval = this.execInterval.bind(this);
+    this.onChanger = this.onChanger.bind(this);
+    this.setInt = 0;
+    this.state = {
+      value: props.value,
+      mouseHolding: false,
+    };
+  }
+  componentDidUpdate() {
+    const isDifferent = this.props.value !== this.state.value;
+    if (isDifferent) this.onChanger(this.props.value);
+  }
+
+  onChanger(value) {
+    const {
+      maxBound,
+      minBound,
+      change,
+    } = this.props;
+    let newValue = value;
+    const isDifferent = newValue !== this.state.value;
+
+    if (newValue <= minBound) {
+      newValue = minBound;
+    } else if (newValue >= maxBound) {
+      newValue = maxBound;
+    }
+
+    const propsIsDifferente = this.props.value !== newValue;
+    if (isDifferent && propsIsDifferente) {
+      this.setState({ value: newValue }, () => {
+        change(newValue);
+      });
+    }
+    if (isDifferent && !propsIsDifferente) this.setState({ value: newValue });
+  }
+
+  increment() {
+    const {
+      step,
+    } = this.props;
+    const increaseZoom = this.state.value + step;
+    this.onChanger(increaseZoom);
+  }
+  decrement() {
+    const {
+      step,
+    } = this.props;
+    const decreaseZoom = this.state.value - step;
+    this.onChanger(decreaseZoom);
+  }
+
+  execInterval(inc) {
+    const exec = inc ? this.increment : this.decrement;
+
+    const interval = () => {
+      clearInterval(this.setInt);
+      this.setInt = setInterval(exec, STEP_TIME);
+    };
+
+    setTimeout(() => {
+      if (this.state.mouseHolding) {
+        interval();
+      }
+    }, DELAY_MILLISECONDS);
+  }
+
+  mouseDownHandler(bool) {
+    this.setState({
+      ...this.state,
+      mouseHolding: true,
+    }, () => {
+      this.execInterval(bool);
+    });
+  }
+
+  mouseUpHandler() {
+    this.setState({
+      ...this.state,
+      mouseHolding: false,
+    }, () => clearInterval(this.setInt));
+  }
+
+  render() {
+    const {
+      value,
+      minBound,
+      maxBound,
+    } = this.props;
+    return (
+      [
+        ZoomTool.renderAriaLabelsDescs(),
+        (<Button
+          key="zoom-tool-1"
+          aria-labelledby="zoomInLabel"
+          aria-describedby="zoomInDesc"
+          role="button"
+          label="-"
+          icon="minus"
+          onClick={() => this.decrement()}
+          disabled={(value <= minBound)}
+          onMouseDown={() => this.mouseDownHandler(false)}
+          onMouseUp={this.mouseUpHandler}
+          onMouseLeave={this.mouseUpHandler}
+          className={styles.prevSlide}
+          hideLabel
+        />),
+        (
+          <span
+            key="zoom-tool-2"
+            aria-labelledby="prevSlideLabel"
+            aria-describedby={this.state.value}
+            className={styles.zoomPercentageDisplay}
+          >
+            {`${this.state.value}%`}
+          </span>
+        ),
+        (<Button
+          key="zoom-tool-3"
+          aria-labelledby="zoomOutLabel"
+          aria-describedby="zoomOutDesc"
+          role="button"
+          label="+"
+          icon="plus"
+          onClick={() => this.increment()}
+          disabled={(value >= maxBound)}
+          onMouseDown={() => this.mouseDownHandler(true)}
+          onMouseUp={this.mouseUpHandler}
+          onMouseLeave={this.mouseUpHandler}
+          className={styles.skipSlide}
+          hideLabel
+        />),
+      ]
+    );
+  }
+}
+
+const propTypes = {
+  value: PropTypes.number.isRequired,
+  change: PropTypes.func.isRequired,
+  minBound: PropTypes.number.isRequired,
+  maxBound: PropTypes.number.isRequired,
+  step: PropTypes.number.isRequired,
+};
+
+ZoomTool.propTypes = propTypes;
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pan-zoom-draw-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pan-zoom-draw-listener/component.jsx
index 6a618a25a41f6776bc2ff3ef1a9bde5be584f103..69fb85479480a87d56200a16b5811d0636c2aca1 100644
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pan-zoom-draw-listener/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pan-zoom-draw-listener/component.jsx
@@ -2,27 +2,182 @@ import React from 'react';
 // import PropTypes from 'prop-types';
 
 export default class PanZoomDrawListener extends React.Component {
+  static calculateDistance({ x: x1, y: y1 }, { x: x2, y: y2 }) {
+    return Math.sqrt(((x1 - x2) ** 2) + ((y1 - y2) ** 2));
+  }
+
   constructor(props) {
     super(props);
-
     this.mouseDownHandler = this.mouseDownHandler.bind(this);
     this.mouseMoveHandler = this.mouseMoveHandler.bind(this);
     this.mouseUpHandler = this.mouseUpHandler.bind(this);
+
+    this.touchMoveHandler = this.touchMoveHandler.bind(this);
+    this.touchStartHandler = this.touchStartHandler.bind(this);
+    this.touchEndHandler = this.touchEndHandler.bind(this);
+
+    this.pinchStartHandler = this.pinchStartHandler.bind(this);
+    this.pinchMoveHandler = this.pinchMoveHandler.bind(this);
+    this.pinchEndHandler = this.pinchEndHandler.bind(this);
+
+    this.panMoveHandler = this.panMoveHandler.bind(this);
+    this.updateTouchPoint = this.updateTouchPoint.bind(this);
+    this.currentMouseX = 0;
+    this.currentMouseY = 0;
+    this.mouseDeltaX = 0;
+    this.mouseDeltaY = 0;
+    this.initialTouches = [];
+    this.state = {
+      pressed: false,
+    };
+    this.pinchGesture = false;
+    this.prevDiff = -1;
+  }
+
+
+  mouseDownHandler(event) {
+    const isLeftClick = event.button === 0;
+    if (isLeftClick) {
+      this.setState({
+        pressed: true,
+      });
+    }
+  }
+
+  updateTouchPoint(event) {
+    if (event.touches.length <= 0) return;
+    const { clientX, clientY } = event.touches[0];
+    this.currentMouseX = clientX;
+    this.currentMouseY = clientY;
+  }
+
+  mouseMoveHandler(event) {
+    if (this.state.pressed) {
+      this.mouseDeltaX = this.currentMouseX - event.clientX;
+      this.mouseDeltaY = this.currentMouseY - event.clientY;
+      this.props.pointChanger(this.mouseDeltaX, this.mouseDeltaY);
+    }
+    this.currentMouseX = event.clientX;
+    this.currentMouseY = event.clientY;
   }
 
-  mouseDownHandler() {
-    this.dummyValue = '';
+  touchStartHandler(event) {
+    this.updateTouchPoint(event);
+    const numberTouches = event.touches.length;
+    if (numberTouches === 2) {
+      this.pinchGesture = true;
+      this.pinchStartHandler(event);
+    }
+    if (numberTouches === 1) {
+      this.pinchGesture = false;
+      this.updateTouchPoint(event);
+    }
   }
 
-  mouseMoveHandler() {
-    this.dummyValue = '';
+  touchMoveHandler(event) {
+    if (this.pinchGesture) {
+      this.pinchMoveHandler(event);
+    }
+    if (!this.pinchGesture) {
+      this.panMoveHandler(event);
+    }
   }
 
-  mouseUpHandler() {
-    this.dummyValue = '';
+  touchEndHandler(event) {
+    if (this.pinchGesture) {
+      this.pinchEndHandler(event);
+      this.pinchGesture = false;
+    } else if (!this.pinchGesture) {
+      this.updateTouchPoint(event);
+    }
+  }
+
+  panMoveHandler(event) {
+    if (this.pinchGesture) return;
+    const { clientX, clientY } = event.changedTouches[0];
+    this.mouseDeltaX = this.currentMouseX - clientX;
+    this.mouseDeltaY = this.currentMouseY - clientY;
+    this.props.pointChanger(this.mouseDeltaX, this.mouseDeltaY);
+    this.currentMouseX = clientX;
+    this.currentMouseY = clientY;
+  }
+
+
+  pinchStartHandler(event) {
+    if (!this.pinchGesture) return;
+    this.initialTouches = [...event.touches];
+    let inputs = [];
+    this.initialTouches.forEach((ev) => {
+      inputs.push({
+        x: ev.clientX,
+        y: ev.clientY,
+      });
+    });
+    this.prevDiff = PanZoomDrawListener.calculateDistance(...inputs);
+    inputs = [];
+    this.updateTouchPoint(event);
+    this.props.touchUpdate(true);
+  }
+
+  pinchMoveHandler(event) {
+    if (!this.pinchGesture) return;
+    if (event.touches.length < 2) return;
+    let inputs = [];
+    [...event.touches].forEach((ev) => {
+      inputs.push({
+        x: ev.clientX,
+        y: ev.clientY,
+      });
+    });
+    const currDiff = PanZoomDrawListener.calculateDistance(...inputs);
+    inputs = [];
+    if (currDiff > 0) {
+      if (currDiff > this.prevDiff) {
+        this.props.zoomChanger(this.props.zoom + (100 * ((currDiff - this.prevDiff) / 100)));
+      }
+      if (currDiff < this.prevDiff) {
+        this.props.zoomChanger(this.props.zoom - (100 * ((this.prevDiff - currDiff) / 100)));
+      }
+    }
+    this.prevDiff = currDiff;
+  }
+
+  pinchEndHandler(event) {
+    this.initialTouches = [];
+    this.updateTouchPoint(event);
+    this.pinchGesture = false;
+    this.props.touchUpdate(false);
+  }
+
+  mouseUpHandler(event) {
+    const isLeftClick = event.button === 0;
+
+    if (isLeftClick) {
+      this.setState({
+        pressed: false,
+      });
+    }
   }
 
   render() {
-    return (<div />);
+    const baseName = Meteor.settings.public.app.basename;
+    const pencilDrawStyle = {
+      width: '100%',
+      height: '100%',
+      touchAction: 'none',
+      zIndex: 2 ** 31 - 1, // maximun value of z-index to prevent other things from overlapping
+      cursor: this.state.pressed ? `url('${baseName}/resources/images/whiteboard-cursor/pan-closed.png') 4 8 ,  default`
+        : `url('${baseName}/resources/images/whiteboard-cursor/pan.png') 4 8,  default`,
+    };
+    return (<div
+      style={pencilDrawStyle}
+      onMouseDown={this.mouseDownHandler}
+      onMouseMove={this.mouseMoveHandler}
+      onMouseUp={this.mouseUpHandler}
+      onMouseLeave={this.mouseUpHandler}
+      onTouchStart={this.touchStartHandler}
+      onTouchMove={this.touchMoveHandler}
+      onTouchEnd={this.touchEndHandler}
+    />);
   }
 }
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/component.jsx
index 0f9c584a2d456b2b879c7c6ce011164a38c6fb26..0c9a8fe51822a1ab8820c490aa944cdabafea693 100755
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/component.jsx
@@ -66,15 +66,15 @@ const runExceptInEdge = fn => (browser().name === 'edge' ? noop : fn);
 class WhiteboardToolbar extends Component {
   constructor() {
     super();
-
+    const isMobile = browser().mobile;
     this.state = {
       // a variable to control which list is currently open
       currentSubmenuOpen: '',
 
       // variables to keep current selected draw settings
       annotationSelected: {
-        icon: 'pen_tool',
-        value: 'pencil',
+        icon: isMobile ? 'hand' : 'pen_tool',
+        value: isMobile ? 'hand' : 'pencil',
       },
       thicknessSelected: { value: 4 },
       colorSelected: { value: '#000000' },
@@ -380,7 +380,7 @@ class WhiteboardToolbar extends Component {
 
   renderThicknessItem() {
     const { intl } = this.props;
-    const isDisabled = this.state.annotationSelected.value === 'pointer';
+    const isDisabled = this.state.annotationSelected.value === 'hand';
     return (
       <ToolbarMenuItem
         disabled={isDisabled}
@@ -453,7 +453,7 @@ class WhiteboardToolbar extends Component {
 
   renderColorItem() {
     const { intl } = this.props;
-    const isDisabled = this.state.annotationSelected.value === 'pointer';
+    const isDisabled = this.state.annotationSelected.value === 'hand';
     return (
       <ToolbarMenuItem
         disabled={isDisabled}
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu/component.jsx
index d46b3ef8e0c5917d1e22b6bc300de47c0ba9ce23..138079f833cd5e57082d9f2f014a98c53480c88f 100755
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu/component.jsx
@@ -7,9 +7,9 @@ import { styles } from '../styles';
 import ToolbarSubmenuItem from '../toolbar-submenu-item/component';
 
 const intlMessages = defineMessages({
-  toolPointer: {
-    id: 'app.whiteboard.toolbar.tools.pointer',
-    description: 'Tool submenu pointer item',
+  toolHand: {
+    id: 'app.whiteboard.toolbar.tools.hand',
+    description: 'Tool submenu hand item',
   },
   toolPencil: {
     id: 'app.whiteboard.toolbar.tools.pencil',
diff --git a/bigbluebutton-html5/imports/utils/slideCalcUtils.js b/bigbluebutton-html5/imports/utils/slideCalcUtils.js
new file mode 100644
index 0000000000000000000000000000000000000000..b2aee0aab47f0d0841f23d5cc829cfcb8caa2c7d
--- /dev/null
+++ b/bigbluebutton-html5/imports/utils/slideCalcUtils.js
@@ -0,0 +1,63 @@
+const HUNDRED_PERCENT = 100;
+const MYSTERY_NUM = 2;
+export default class SlideCalcUtil {
+  // After lots of trial and error on why synching doesn't work properly, I found I had to 
+  // multiply the coordinates by 2. There's something I don't understand probably on the
+  // canvas coordinate system. (ralam feb 22, 2012)
+
+  /**
+   * Calculate the viewed region width
+   */
+  static calcViewedRegionWidth(vpw, cpw) {
+    const width = (vpw / cpw) * HUNDRED_PERCENT;
+    if (width > HUNDRED_PERCENT) {
+      return HUNDRED_PERCENT;
+    }
+    return width;
+  }
+
+  static calcViewedRegionHeight(vph, cph) {
+    const height = (vph / cph) * HUNDRED_PERCENT;
+    if (height > HUNDRED_PERCENT) {
+      return HUNDRED_PERCENT;
+    }
+    return height;
+  }
+
+  static calcCalcPageSizeWidth(ftp, vpw, vrw) {
+    if (ftp) {
+      return (vpw / vrw) * HUNDRED_PERCENT;
+    }
+      return vpw;
+  }
+
+  static calcCalcPageSizeHeight(ftp, vph, vrh, cpw, cph, opw, oph) {
+    if (ftp) {
+      return (vph / vrh) * HUNDRED_PERCENT;
+    }
+      return (cpw / opw) * oph;
+  }
+
+  static calcViewedRegionX(cpx, cpw) {
+    return (cpx * HUNDRED_PERCENT) / cpw || 0;
+  }
+
+  static calcViewedRegionY(cpy, cph) {
+    return (cpy * HUNDRED_PERCENT) / cph || 0;
+  }
+
+  static calculateViewportX(vpw, pw) {
+    if (vpw == pw) {
+      return 0;
+    }
+      return (pw - vpw) / MYSTERY_NUM;
+  }
+
+  static calculateViewportY(vph, ph) {
+    if (vph == ph) {
+      return 0;
+    }
+    return (ph - vph) / MYSTERY_NUM;
+    
+  }
+}
\ No newline at end of file
diff --git a/bigbluebutton-html5/private/config/settings-development.json b/bigbluebutton-html5/private/config/settings-development.json
index 710c0984c599a6fbce107bb4abea9a0a5ad864bd..f0f40fa656d4cb151247de4dcdf4b3411ab61ff2 100755
--- a/bigbluebutton-html5/private/config/settings-development.json
+++ b/bigbluebutton-html5/private/config/settings-development.json
@@ -150,6 +150,7 @@
           "sendAnnotation",
           "removePresentation",
           "setPresentation",
+          "zoomSlide",
           "requestPresentationUploadToken"
         ]
       }
@@ -353,8 +354,8 @@
             "value": "pencil"
           },
           {
-            "icon": "pointer",
-            "value": "pointer"
+            "icon": "hand",
+            "value": "hand"
           }
         ]
       }
diff --git a/bigbluebutton-html5/private/config/settings-production.json b/bigbluebutton-html5/private/config/settings-production.json
index 67a3c18088adc3076487836c5f9581fac78000df..40cde8bdf4b56dbc451210c8db2e78eba2d9f129 100755
--- a/bigbluebutton-html5/private/config/settings-production.json
+++ b/bigbluebutton-html5/private/config/settings-production.json
@@ -150,6 +150,7 @@
           "sendAnnotation",
           "removePresentation",
           "setPresentation",
+          "zoomSlide",
           "requestPresentationUploadToken"
         ]
       }
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json
index f4a01c30d4a5840d9763a1269c6cdab8c71975a8..11707bbef9a6d3179d6b8db1014b265f9075f44a 100755
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/private/locales/en.json
@@ -57,6 +57,11 @@
     "app.presentation.presentationToolbar.fitScreenDesc": "Display the whole slide",
     "app.presentation.presentationToolbar.zoomLabel": "Zoom",
     "app.presentation.presentationToolbar.zoomDesc": "Change the zoom level of the presentation",
+    "app.presentation.presentationToolbar.zoomInLabel": "Zoom in",
+    "app.presentation.presentationToolbar.zoomInDesc": "Zoom in the presentation",
+    "app.presentation.presentationToolbar.zoomOutLabel": "Zoom out",
+    "app.presentation.presentationToolbar.zoomOutDesc": "Zoom out of the presentation",
+    "app.presentation.presentationToolbar.zoomIndicator": "Show the zoom percentage",
     "app.presentation.presentationToolbar.goToSlide": "Slide {0}",
     "app.presentationUploder.title": "Presentation",
     "app.presentationUploder.message": "As a presenter in BigBlueButton, you have the ability of uploading any office document or PDF file. We recommend for the best results, to please upload a PDF file.",
@@ -361,7 +366,7 @@
 
     "app.meeting.endNotification.ok.label": "OK",
     "app.whiteboard.toolbar.tools": "Tools",
-    "app.whiteboard.toolbar.tools.pointer": "Pointer",
+    "app.whiteboard.toolbar.tools.hand": "Hand",
     "app.whiteboard.toolbar.tools.pencil": "Pencil",
     "app.whiteboard.toolbar.tools.rectangle": "Rectangle",
     "app.whiteboard.toolbar.tools.triangle": "Triangle",
diff --git a/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan-closed.png b/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan-closed.png
new file mode 100644
index 0000000000000000000000000000000000000000..0122a1afc817b008d51a48a4726a2ec6c438c175
Binary files /dev/null and b/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan-closed.png differ
diff --git a/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan-closed.svg b/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan-closed.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6c37077be908000f14139a6a4f5810444569c04c
--- /dev/null
+++ b/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan-closed.svg
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1"
+	 id="svg2" inkscape:export-filename="/home/cwalton/Pictures/bbb-cursors/triangle.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90" inkscape:version="0.91 r13725" sodipodi:docname="triangle.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
+	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 32 32"
+	 style="enable-background:new 0 0 32 32;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:none;stroke:#FF0000;stroke-miterlimit:10;}
+	.st1{fill:#FFFFFF;stroke:#3F3F3F;stroke-miterlimit:10;}
+</style>
+<sodipodi:namedview  bordercolor="#666666" borderopacity="1.0" id="base" inkscape:bbox-nodes="true" inkscape:bbox-paths="true" inkscape:current-layer="layer1" inkscape:cx="25.698868" inkscape:cy="14.972468" inkscape:document-units="px" inkscape:guide-bbox="true" inkscape:object-nodes="true" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:snap-bbox="true" inkscape:snap-grids="true" inkscape:snap-nodes="false" inkscape:snap-others="true" inkscape:snap-smooth-nodes="true" inkscape:window-height="1009" inkscape:window-maximized="1" inkscape:window-width="1920" inkscape:window-x="0" inkscape:window-y="34" inkscape:zoom="16" pagecolor="#ffffff" showgrid="true" showguides="false" units="px">
+	<inkscape:grid  id="grid4203" type="xygrid"></inkscape:grid>
+	<sodipodi:guide  id="guide4258" orientation="1,0" position="2,25"></sodipodi:guide>
+	<sodipodi:guide  id="guide4260" orientation="0,1" position="8,30"></sodipodi:guide>
+	<sodipodi:guide  id="guide4262" orientation="1,0" position="26,21"></sodipodi:guide>
+	<sodipodi:guide  id="guide4264" orientation="0,1" position="9,6"></sodipodi:guide>
+	<sodipodi:guide  id="guide4269" orientation="0,1" position="31,24"></sodipodi:guide>
+	<sodipodi:guide  id="guide4271" orientation="1,0" position="8,21"></sodipodi:guide>
+</sodipodi:namedview>
+<path class="st0" d="M28.3,13.1"/>
+<path class="st1" d="M9.9,11.8c-0.2-0.7,0-1.6,0.8-1.9c0.4-0.1,0.7-0.1,1,0.1c0.3,0.2,0.4,0.7,0.9,0.5c0.4-0.2,0.3-1.1,0.8-1.4
+	c0.8-0.6,1.8,0,2,0.8c0,0.2,0,0.8,0.4,0.8c0.3,0,0.3-0.3,0.4-0.5c0.4-0.7,1.4-0.8,1.9-0.1c0.5,0.6,0,1.5,0.4,1.7
+	c0.3,0.2,0.5-0.1,0.9-0.2c0.4-0.1,1.1,0,1.4,0.4c0.8,1-0.2,3.9-0.6,5c-0.7,2-1.6,4.1-3.6,5.1c-3.6,1.7-6.9-1.9-8-3.4
+	c-0.3-0.5-0.7-1.3-1-1.8c-0.5-1.2,0.7-2.7,2-2.1c0.3,0.1,0.6,0.8,0.9,0.7c0.4-0.1,0.1-0.7,0.1-1C10.4,13.6,10.2,12.7,9.9,11.8z"/>
+<path class="st0" d="M47.3,14.2"/>
+</svg>
diff --git a/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan.png b/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan.png
new file mode 100644
index 0000000000000000000000000000000000000000..5bff2d6cb58102d3e2c17344c1f34122c22b3db1
Binary files /dev/null and b/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan.png differ
diff --git a/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan.svg b/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan.svg
new file mode 100644
index 0000000000000000000000000000000000000000..c69f0479626567ae978dfcd54b21b96e36f41b3b
--- /dev/null
+++ b/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1"
+	 id="svg2" inkscape:export-filename="/home/cwalton/Pictures/bbb-cursors/triangle.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90" inkscape:version="0.91 r13725" sodipodi:docname="triangle.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
+	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 32 32"
+	 style="enable-background:new 0 0 32 32;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#FFFFFF;stroke:#3F3F3F;stroke-miterlimit:10;}
+	.st1{fill:none;stroke:#FF0000;stroke-miterlimit:10;}
+</style>
+<path class="st0" d="M20,17.3l2.1-7.5c0,0,0.4-1.2-0.7-1.5c0,0-1.2-0.5-1.6,0.9L19,11.7c0,0-0.2,0.8-0.6,0.7c0,0-0.4,0-0.2-1l1-5.8
+	c0,0,0.2-1-1-1.2c0,0-1.2-0.3-1.4,0.7l-0.8,5c0,0-0.1,0.9-0.4,0.9c0,0-0.3,0.1-0.3-0.9l-0.1-6.8c0,0,0.1-0.9-1.2-0.9
+	c0,0-1.1,0.1-1.1,0.9l0,6.5c0,0,0.1,1-0.3,1c0,0-0.5,0.2-0.7-1L11.1,5c0,0-0.2-0.9-1.3-0.7c0,0-1,0.1-1,1.1l1.3,8.3
+	c0,0,0.2,0.9-0.5,1.4c0,0-0.8,0.5-1.8-0.2l-2.2-1.9c0,0-1-0.8-2,0.5c0,0-0.7,0.8,0.4,1.9c0,0,6.3,6.9,9.8,6.9
+	C13.9,22.5,18.5,23.4,20,17.3z"/>
+</svg>