Zoom and crop YouTube videos to fill screen height

Removes letterboxing by cropping the left and right edges off of full-screen YouTube videos and zooming to fill the screen height.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name            Zoom and crop YouTube videos to fill screen height
// @version         0.6.4
// @description     Removes letterboxing by cropping the left and right edges off of full-screen YouTube videos and zooming to fill the screen height.
// @author          uxamend
// @namespace       https://greasyfork.org/en/users/231373-uxamend
// @match           https://www.youtube.com/*
// @match           https://www.youtube-nocookie.com/embed/*
// @exclude-match   https://www.youtube.com/ad_frame*
// @exclude-match   https://www.youtube.com/ad_companion*
// @exclude-match   https://www.youtube.com/embed/
// @exclude-match   https://www.youtube.com/video_masthead*
// @grant           none
// @run-at          document-idle
// @license         CC0-1.0
// @compatible      firefox version >=64 (older versions untested)
// @compatible      chrome version >=71 (older versions untested)
// ==/UserScript==

// In @exclude-matches above, we want to exclude exactly
// https://www.youtube.com/embed/, but not https://www.youtube.com/embed/*
// /embed/ without a subdirectory appears in the background of normal YouTube,
// whereas /embed/* is for embedded videos on other sites.

"use strict";

// ==================================
//   User-defined global parameters
// ==================================

// For tall screens: Sets the narrowest aspect ratio that videos will ever be
// cropped to.
var min_cropped_aspect = 16/10; // Express as a fraction, e.g. 16/10, not 16:10.

// For very wide videos: The maximum proportion of video width to crop off
var max_crop_proportion = 1;

// Sets the aspect ratio of the player for using zoom outside of full-screen.
// This has no effect in full-screen mode.
// To use the screen aspect ratio as the player aspect ratio, set this to zero.
var player_aspect = 16/9; // Set to zero or a fraction, e.g. 16/9, not 16:9.

// Default zoom state outside full-screen (true = enabled, false = disabled)
var def_zoom_n = false;

// Default zoom state in full-screen (true = enabled, false = disabled)
var def_zoom_f = true;

// Sets which key will be used to enable and disable zoom
var zoom_shortcut_key = "z";

// ==================================


var debug_logging_on = false;
var debug_script_name = "Userscript: Zoom YouTube videos to fill screen height"

function debug_log(message) {
  if(debug_logging_on){
    console.log("[" + debug_script_name + "] " + message);
  }
}

/**
 * set_zoom
 * Zooms a specified video to fill specified containing area dimensions.
 * Parameters:
 *     video   video to zoom
 *     cw      containing area width
 *     ch      containing area height
 * Global parameters read:
 *     min_cropped_aspect    minimum aspect ratio to crop videos down to
 *     max_crop_proportion   maximum proportion of video width to crop away
 */
function set_zoom(video, cw, ch) {
  var vs = video.style;
  var video_aspect = video.videoWidth/video.videoHeight;
  var containing_aspect = cw/ch;

  // Don't zoom if the endscreen is showing
  if(!video.ended) {
    // Only zoom and crop videos that are wide enough to crop
    if(video_aspect > containing_aspect && video_aspect > min_cropped_aspect) {
      debug_log("Video is wider than containing area and min_cropped_aspect. Setting zoom.");

      var vh = ch; // vh = video height

      // Apply min_cropped_aspect constraint to video height
      if (min_cropped_aspect > containing_aspect) vh = cw/min_cropped_aspect;

      var vw = video_aspect * vh; // vw = video width (including cropped portion)

      // Apply max_crop_proportion constraint to video width
      if (cw/vw < 1-max_crop_proportion) vw = cw + vw * max_crop_proportion;

      var vt = (ch-vh)/2; // vt = top edge position of video
      var vl = (cw-vw)/2; // vl = left edge position of video

      debug_log("Containing area dimensions: " + cw + "x" + ch + ".");
      debug_log("Calculated new video element dimensions: " + vw + "x" + vh + ", origin at " + vl + ", " + vt + ".");
      debug_log("(Underlying video stream resolution: " + video.videoWidth + "x" + video.videoHeight + ".)");
      debug_log("containing_aspect: " + containing_aspect + ".");
      debug_log("min_cropped_aspect: " + min_cropped_aspect + "." );
      debug_log("video_aspect: " + video_aspect + ".");
      debug_log("max_crop_proportion: " + max_crop_proportion + ".");

      // This might appear to risk creating an endless loop via the mutation
      // observer. However, it doesn't. I'm guessing that changing the dimensions
      // doesn't constitute a mutation, but if it does it can result in at most one
      // superfluous execution of set_zoom(). If the first execution causes a
      // mutation by changing the video element's dimensions, then the second
      // execution, if it is surplus to requirements, should set them to the same
      // values, resulting in no mutation and no third execution (until genuinely
      // needed).
      vs.height = vh + "px";
      vs.width = vw + "px";
      vs.top = vt + "px";
      vs.left = vl + "px";

    } else {
      debug_log("Video is not wide enough to require zoom ("
                + video.videoWidth
                + "x"
                + video.videoHeight
                + "). Not setting zoom.");
      unzoom(video, cw, ch);
    }
  } else {
    debug_log("Video has ended. Not setting zoom. (Otherwise we mess with the endscreen.)");
  }
}

/**
 * unzoom
 * Undoes the visual effects of set_zoom.
 * Note that unzoom does not gurantee to return the video dimensions exactly to
 * their original values, but the visual appearance should be the same (or near
 * enough as makes no odds).
 * Parameters:
 *     video   video to unzoom
 *     cw      containing area width
 *     ch      containing area height
 */
function unzoom(video, cw, ch) {
  // It would be better to somehow trigger YouTube's standard video sizing, but
  // in the absence of a way to trigger that, we'll just do this.
  
  var vs = video.style;
  var video_aspect = video.videoWidth/video.videoHeight;
  var containing_aspect = cw/ch;
  
  // Don't unzoom if the endscreen is showing
  if(!video.ended) {
    debug_log("Unzooming video.");
    
    // Usually the player is sized to fit the video exactly in default view,
    // but not for narrow videos, which are pillarboxed with white bars. Rarely,
    // the player defaults to 16:9 for all videos, so that wide videos are
    // letterboxed with white bars.
    // 
    // In theater mode and full-screen mode, the player has a fixed aspect and
    // the video is letter- or pillarboxed with black bars if it doesn't fit
    // exactly.
    // 
    // To avoid black bars in default view, we must size the video to fill the
    // container in the video's longest dimension only. (Otherwise we could
    // just size it to fill the container in both dimensions.)
    var w, h, t, l;
    if(video_aspect == containing_aspect) {
      // video that fits the container exactly
      w = cw; h = ch; t = 0; l = 0;
    } else if(video_aspect > containing_aspect) {
      // letterboxed video
      w = cw; l = 0;
      h = cw / video_aspect;
      t = (ch - h) / 2;
    } else {
      // pillarboxed video
      h = ch; t = 0;
      w = ch * video_aspect;
      l = (cw - w) / 2;
    }
    
    vs.width = w + "px";
    vs.height = h + "px";
    vs.top = t + "px";
    vs.left = l + "px";
  } else {
    debug_log("Video has ended. Not unzooming. (Otherwise we mess with the endscreen.)");
  }
}

/**
 * in_theater_mode
 * Returns true if we're in Theater mode.
 */
function in_theater_mode() {
  return (document.getElementById("player-theater-container") &&
          document.getElementById("player-theater-container").childElementCount > 0 &&
          !document.fullscreenElement);
}

/**
 * set_player_aspect
 * Changes the aspect ratio of the video player element to the specified aspect
 * ratio, as interpreted by YouTube's default CSS.
 * Parameters:
 *     aspect            aspect ratio to use
 *     theater_default   if true, set theater mode to the default aspect ratio
 *                       instead of the specified aspect ratio
 */
function set_player_aspect(aspect, theater_default=false) {
  debug_log("Setting player aspect to " + aspect + ".");
  
  // We need to set overflow to hidden on the movie-player otherwise the video
  // overhangs in miniplayer mode. Get it by class name rather than id, for
  //  compatibility with embedded videos
  document.getElementsByClassName("html5-video-player")[0].style.setProperty("overflow", "hidden");
  
  // For embedded videos, we don't need to do anything.
  
  // For default view
  if(document.getElementsByTagName("ytd-watch-flexy")[0]) {
    var ytdwfs = document.getElementsByTagName("ytd-watch-flexy")[0].style;
    ytdwfs.setProperty("--ytd-watch-flexy-width-ratio", aspect);
    ytdwfs.setProperty("--ytd-watch-flexy-height-ratio", 1);
  }
  
  // For theater mode
  var ptc = document.getElementById("player-theater-container");
  
  if(in_theater_mode() && !theater_default) {
    debug_log("Setting theater mode height.");
    // 56px for masthead; --ytd-masthead-height is not always set, so can't use
    // that unfortunately
    ptc.style.setProperty("max-height", "calc(100vh - 56px)");
    ptc.style.setProperty("height", "calc((" + (1/aspect) + ") * 100vw)");
  } else {
    debug_log("Unsetting theater mode height.");
    if(ptc) {
      ptc.style.removeProperty("max-height");
      ptc.style.removeProperty("height");
    }
  }
}

/**
 * apply_player_aspect
 * To facilitate zoom and crop when the movie_player is not full-screen, this sets
 * the aspect ratio of the movie_player to follow the player_aspect setting.
 * Calling with the zoom parameter set to false returns the player to the YouTube
 * default of matching the video aspect ratio.
 * Parameters:
 *     zoom   if true, use player_aspect
 *            if false, use the actual video aspect (YouTube default)
 * Global parameters read:
 *     player_aspect   the aspect ratio to use, or zero (indicating to use the
 *                     screen aspect)
 */
function apply_player_aspect(zoom=true) {
  var video = document.getElementsByClassName("html5-main-video")[0];
  var video_aspect = video.videoWidth/video.videoHeight;
  
  if(zoom) {
    if(player_aspect == 0) {
      debug_log("Adjusting player aspect ratio to match screen.");
      var screen_aspect = window.screen.width/window.screen.height;
      if(video_aspect > screen_aspect) {
        set_player_aspect(screen_aspect);
      } else {
        debug_log("No need to change player aspect; video is not wide enough.");
      }
    } else {
      debug_log("Adjusting player aspect ratio to configured value.");
      if(video_aspect > player_aspect) {
        set_player_aspect(player_aspect); 
      } else {
        debug_log("No need to change player aspect; video is not wide enough.");
      }
    }
  } else {
    debug_log("Restoring player aspect ratio to match video.");
    set_player_aspect(video_aspect, true);
    // N.B. If video_aspect is narrow, the expected behaviour of set_player_aspect
    // is that YouTube's CSS may result in the video being pillarboxed, due to the
    // maximum height constraint.
  }
}

/**
 * set_zoom_to_window
 * Zooms a video to fill the window dimensions.
 * Parameters:
 *     video   the video to set zoom for
 *     zoom    if false, will unzoom instead of setting zoom
 * Global parameters read:
 *     min_cropped_aspect    minimum aspect ratio to crop videos down to
 *     max_crop_proportion   maximum proportion of video width to crop away
 */
function set_zoom_to_window(video, zoom=true) {
  if(zoom) {
    set_zoom(video,
           window.innerWidth,
           window.innerHeight);
  } else {
    unzoom(video,
           window.innerWidth,
           window.innerHeight);
  }
}

/**
 * set_zoom_to_movie_player
 * Zooms a video to fill its containing movie_player element in the DOM. When not in
 * full-screen mode, also changes the size of the movie_player to follow the
 * player_aspect setting (else there'll be no zoom and crop).
 * Parameters:
 *     video   the video to set zoom for
 *     zoom    if false, will unzoom instead of setting zoom
 * Global parameters read:
 *     min_cropped_aspect    minimum aspect ratio to crop videos down to
 *     max_crop_proportion   maximum proportion of video width to crop away
 *     player_aspect         aspect ratio setting for non-full-screen movie_player
 */
function set_zoom_to_movie_player(video, zoom=true) {
  if(!document.fullscreenElement) {
    // The movie_player is the grandparent node of the video element.
    // Open question: Is it more likely for the ID of the relevant element to
    // change (so that selecting it as the grandparent is the best strategy), or
    // for its position in the DOM tree to change (so that selecting it by ID is
    // the best strategy)?
    if(zoom) {
      apply_player_aspect(true);
      set_zoom(video,
               video.parentNode.parentNode.clientWidth,
               video.parentNode.parentNode.clientHeight);
    } else {
      unzoom(video,
             video.parentNode.parentNode.clientWidth,
             video.parentNode.parentNode.clientHeight);
    }
  } else {
    apply_player_aspect(false);
    
    // In full-screen mode, the movie-player is not necessarily the same size as
    // the screen, which can cause a slight offset. Use set_zoom_to_window instead
    // for this case.
    set_zoom_to_window(video, zoom);
  }
  zoom_button.update();
}

/**
 * mo_callback
 * Callback function for mutation observer, to re-apply zoom if the video element
 * mutates. E.g. when an ad starts or stops playing, or in other circumstances
 * when the video might change dimensions or become reset to its default,
 * letterboxed state.
 */
function mo_callback(mutation_list, observer) {
  mutation_list.forEach((mutation) => {
    if(mutation.type == "attributes"){
      
      // We have to check whether zoom "should" be on, because the
      // fullscreenchange event may not be fast enough, in which case we will
      // catch the mutations caused by exiting full-screen.
      if(zoom_should_be_on()) {
        debug_log("Video element mutated.");
        set_zoom_to_movie_player(mutation.target);
      } else {
        debug_log("Video element mutated but zoom should be off.");
        zoom_off();
      }
    }
  });
}

var mo = new MutationObserver(mo_callback);

function observe_video_mutations(video) {
  mo.observe(video, {"attributes" : true});
}

/**
 * zoom_on
 * Unconditionally apply zoom and keep it applied until zoom_off is called.
 */
function zoom_on() {
  debug_log("Turning zoom on.");
  var video = document.getElementsByClassName("html5-main-video")[0];
  set_zoom_to_movie_player(video);
  observe_video_mutations(video);
}

/**
 * zoom_off
 * Unconditionally stop applying zoom, until zoom_on is called.
 */
function zoom_off() {
  debug_log("Turning zoom off.");
  mo.disconnect();
  var video = document.getElementsByClassName("html5-main-video")[0];
  apply_player_aspect(false);
  set_zoom_to_movie_player(video, false);
}

// Manual zoom state outside full-screen
var man_enab_n = def_zoom_n;

// Manual zoom state in full-screen
var man_enab_f = def_zoom_f;

/**
 * zoom_should_be_on
 * Returns true if we're in a state where zoom is supposed to currently be
 * enabled, else false.
 */
function zoom_should_be_on() {
  return ((man_enab_n && !document.fullscreenElement)
       || (man_enab_f && document.fullscreenElement));
}

/**
 * zoom_on_or_off
 * Puts zoom into the correct on/off state, as per zoom_should_be_on.
 */
function zoom_on_or_off() {
  if(zoom_should_be_on()) {
    setTimeout(zoom_on, 200);
  } else {
    zoom_off();
  }
}

/**
 * toggle_manual_enab
 * Changes the manual override zoom state for the current display mode; either
 * full-screen or non-full-screen.
 */
function toggle_manual_enab() {
  debug_log("Toggling manual enable state.");
  if(document.fullscreenElement){
    man_enab_f = !man_enab_f;
    if(man_enab_f) debug_log("Set full-screen zoom enabled.");
    else debug_log("Set full-screen zoom disabled.");
  } else {
    man_enab_n = !man_enab_n;
    if(man_enab_n) debug_log("Set non-full-screen zoom enabled.");
    else debug_log("Set non-full-screen zoom disabled.");
  }
  zoom_on_or_off();
}

/**
 * handle_keydown
 * Event handler for any keydown events, to trigger appropriate actions.
 */
function handle_keydown(e) {
  debug_log('"' + e.key + '" key was pressed.');
  if(e.key == zoom_shortcut_key.toLowerCase()) toggle_manual_enab();
  if(e.key == zoom_shortcut_key.toUpperCase()) toggle_manual_enab();
  if(e.key == "i") {
    // Workaround for bug: exiting miniplayer directly into normal view does not
    // trigger reapplication of zoom. Pressing 'i' seems to be the only way to
    // trigger this bug, so detecting the pressing of 'i' seems like a good way
    // to fix it.
    zoom_on_or_off();
  }
}

/**
 * watch_for_fullscreen
 * Start watching for changes to the full-screen state, and make sure the correct
 * zoom state is applied at each transition of full-screen state.
 * N.B. There may be a slight delay in reaction to changes in full-screen state.
 */
function watch_for_fullscreen() {
  debug_log("Adding fullscreenchange event listener.");
  document.addEventListener(
    'fullscreenchange',
    function() {
      debug_log("Full-screen state changed.");
      zoom_on_or_off();
    }
  );
}

/**
 * watch_for_keypresses
 * Start watching for keydown events and handle them when they occur.
 */
function watch_for_keypresses() {
  debug_log("Adding keydown event listener.");
  document.addEventListener(
    'keydown',
    handle_keydown
  );
}

/**
 * create_zoom_button
 * Adds a zoom button to the YouTube player controls, which toggles manual override of
 * zoom state.
 * Returns:
 *     an object representing the button
 */
function create_zoom_button() {
  var right_controls;
  var size_button;
  var tooltip_showing = false;
  var button;
  var icon_path;

  /**
   * set_zoom_button_mode
   * Sets the zoom button to an appropriate mode for the current zoom state.
   */
  function set_zoom_button_mode() {
    var l;

    if(zoom_should_be_on()) {
      icon_path.setAttribute("d",
  "m 8,11 0,14 20,0 0,-14 -20,0 z m 2,4 16,0 0,6 -16,0 0,-6 z"
                       );
      l = "Letterbox (" + zoom_shortcut_key + ")"
    } else {
      icon_path.setAttribute("d",
  "m 4,11 0,14 3,0 0,-2 -1,0 0,-10 1,0 0,-2 -3,0 z\
   m 4,0 0,14 20,0 0,-14 -20,0 z\
   m 21,0 0,2 1,0 0,10 -1,0 0,2 3,0 0,-14 -3,0 z\
   m -19,2 16,0 0,10 -16,0 0,-10 z"
                       );
      l = "Zoom and crop (" + zoom_shortcut_key + ")"
    }

    button.setAttribute("aria-label", l);
    button.setAttribute("title", l);
  }
  
  /**
   * create_zoom_button_icon
   * Adds the icon to the zoom button during initial creation of the button.
   */
  function create_zoom_button_icon() {
    // Create icon SVG element
    var s = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    s.setAttribute("height", "100%");
    s.setAttribute("version", "1.1");
    s.setAttribute("viewBox", "0 0 36 36");
    s.setAttribute("width", "100%");

    var p_id = "zac-path-1";

    // Apply shadow
    var sh = document.createElementNS("http://www.w3.org/2000/svg", "use");
    sh.setAttribute("class", "ytp-svg-shadow");
    sh.setAttribute("href", "#" + p_id);

    // Create icon path
    var p = document.createElementNS("http://www.w3.org/2000/svg", "path");
    p.setAttribute("id", p_id);
    p.setAttribute("class", "ytp-svg-fill");

    // Append path and shadow to SVG
    s.appendChild(sh);
    s.appendChild(p);

    // Append icon to button
    button.appendChild(s);

    icon_path = p;
  }
  
  /**
   * show_zoom_button_tooltip
   * Shows the tooltip associated with the zoom button in a style mimicking that
   * of the other YouTube player buttons.
   * Parameters:
   *     show     if false, the tooltip will be hidden instead of shown
   */
  function show_zoom_button_tooltip(show=true) {
    // Position calculations
    var bbcr = button.getBoundingClientRect();
    var tt_horiz_cen = bbcr.left + bbcr.width/2; // tooltip horizontal centre

    var tt_top_offset = 57; // How far above the button should the tooltip be?

    // For some reason, the existing tooltips are at a different offset in full-screen.
    if(document.fullscreenElement) {
      tt_top_offset = 75;
    }

    var tt_top = bbcr.top + bbcr.height/2 - tt_top_offset; // tooltip top

    // YouTube has an existing tooltip DOM structure that it reuses for all of its
    // player tooltips, but it's easier and more reliable to just create our own,
    // using the same classes.

    // Try to get our existing tooltip from DOM from previous run
    var tt = document.getElementById("zac-tt");

    var tt_text;

    if(!tt) {
      // Create tool-tip DOM structure if not present.
      var mp = document.getElementsByClassName("html5-video-player")[0];
      var tt_text_wrapper = document.createElement("div");
      tt = document.createElement("div");
      tt_text = document.createElement("span");

      tt.setAttribute("class", "ytp-tooltip ytp-bottom");
      tt.setAttribute("id", "zac-tt");
      tt.style.setProperty("position", "fixed");

      tt_text_wrapper.setAttribute("class", "ytp-tooltip-text-wrapper");

      tt_text.setAttribute("class", "ytp-tooltip-text");
      tt_text.setAttribute("id", "zac-tt-text");

      tt.appendChild(tt_text_wrapper);
      tt_text_wrapper.appendChild(tt_text);
      mp.appendChild(tt);
    } else {
      // If DOM structure already present, get tooltip text.
      tt_text = document.getElementById("zac-tt-text");
    }

    if(show) { // show
      tt.style.setProperty("top", tt_top + "px");
      tt_text.innerHTML = button.getAttribute("aria-label");
      tt.style.removeProperty("display"); // show the tooltip

      // Calculate horizontal position. Tooltip must be showing before
      // its width can be queried.
      var tt_width = tt.getBoundingClientRect().width;
      tt.style.setProperty("left", tt_horiz_cen - tt_width / 2 + "px");
      debug_log("tt_width = " + tt_width);
      debug_log("tt_horiz_cen = " + tt_horiz_cen);
      debug_log("tt left position = " + (tt_horiz_cen - tt_width / 2));

      // Remove button title, else the browser may (will) display it as a
      // tooltip, in addition to ours.
      button.removeAttribute("title");
    } else { // hide
      tt.style.setProperty("display", "none");
      tt_text.innerHTML = "";
      button.setAttribute("title", button.getAttribute("aria-label"));
    }

    tooltip_showing = show;

    // All of that just for a tooltip that matches the others. And it's
    // still not perfect. Sheesh.
  }
  
  /**
   * update
   * Ensures the zoom button reflects the current state.
   */
  function update() { 
    set_zoom_button_mode();
    show_zoom_button_tooltip(tooltip_showing);
  }
  
  var button_object;
  
  function add_button() {
    right_controls = document.getElementsByClassName("ytp-right-controls")[0];
    size_button = document.getElementsByClassName("ytp-size-button") [0];
    
    if(right_controls && size_button) {
      debug_log("Adding zoom and crop toggle button.")
      
      // Remove existing button if present (sometimes it persists even after a page reload)
      var existing_button = document.getElementById("zac-zoom-button");
      if(existing_button) {
        debug_log("Destroying old zoom and crop toggle button.")
        right_controls.removeChild(existing_button);
      }
      
      // Create button
      button = document.createElement("button");
      button.setAttribute("class", "ytp-button");
      button.setAttribute("id", "zac-zoom-button");

      create_zoom_button_icon();
      set_zoom_button_mode();

      // Add button to controls
      right_controls.insertBefore(button, size_button);

      // Set event handlers
      button.addEventListener("click", toggle_manual_enab);
      button.addEventListener("mouseover", function(){show_zoom_button_tooltip();});
      button.addEventListener("mouseout", function(){show_zoom_button_tooltip(false);});
      button.addEventListener("focus", function(){show_zoom_button_tooltip();});
      button.addEventListener("blur", function(){show_zoom_button_tooltip(false);});

      button_object = { 
        update : update
      }

    } else {
      // Keep trying until we have somewhere to put the button.
      debug_log("Can't add zoom and crop toggle button yet. Retrying in 200ms.")
      setTimeout(add_button, 200);
    }
  }
  
  add_button();
  return button_object;
}


// Initialise
watch_for_fullscreen();
watch_for_keypresses();
var zoom_button = create_zoom_button();