Image Carousel

NOTICE:

This is a component page used internally by the SCP Wiki. It is intended to be used and included on other pages.

rating: +18+x

What this is

A bunch of miscellaneous CSS 'improvements' that I, CroquemboucheCroquembouche, use on a bunch of pages because I think it makes them easier to deal with.

The changes this component makes are bunch of really trivial modifications to ease the writing experience and to make documenting components/themes a bit easier (which I do a lot). It doesn't change anything about the page visually for the reader — the changes are for the writer.

I wouldn't expect translations of articles that use this component to also use this component, unless the translator likes it and would want to use it anyway.

This component probably won't conflict with other components or themes, and even if it does, it probably won't matter too much.

Usage

On any wiki:

[[include :scp-wiki:component:croqstyle]]

This component is designed to be used on other components. When using on another component, be sure to add this inside the component's [[iftags]] block, so that users of your component are not forced into also using Croqstyle.

Related components

Other personal styling components (which change just a couple things):

Personal styling themes (which are visual overhauls):

CSS changes

Reasonably-sized footnotes

Stops footnotes from being a million miles wide, so that you can actually read them.

.hovertip { max-width: 400px; }

Monospace edit/code

Makes the edit textbox monospace, and also changes all monospace text to Fira Code, the obviously superior monospace font.

@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&display=swap');
 
:root { --mono-font: "Fira Code", Cousine, monospace; }
#edit-page-textarea, .code pre, .code p, .code, tt, .page-source { font-family: var(--mono-font); }
.code pre * { white-space: pre; }
.code *, .pre * { font-feature-settings: unset; }

Teletype backgrounds

Adds a light grey background to <tt> elements ({{text}}), so code snippets stand out more.

tt {
  background-color: var(--swatch-something-bhl-idk-will-fix-later, #f4f4f4);
  font-size: 85%;
  padding: 0.2em 0.4em;
  margin: 0;
  border-radius: 6px;
}

No more bigfaces

Stops big pictures from appearing when you hover over someone's avatar image, because they're stupid and really annoying and you can just click on them if you want to see the big version.

.avatar-hover { display: none !important; }

Breaky breaky

Any text inside a div with class nobreak has line-wrapping happen between every letter.

.nobreak { word-break: break-all; }

Code colours

Add my terminal's code colours as variables. Maybe I'll change this to a more common terminal theme like Monokai or something at some point, but for now it's just my personal theme, which is derived from Tomorrow Night Eighties.

Also, adding the .terminal class to a fake code block as [[div class="code terminal"]] gives it a sort of pseudo-terminal look with a dark background. Doesn't work with [[code]], because Wikidot inserts a bunch of syntax highlighting that you can't change yourself without a bunch of CSS. Use it for non-[[code]] code snippets only.

Quick tool to colourise a 'standard' Wikidot component usage example with the above vars: link

:root {
  --c-bg: #393939;
  --c-syntax: #e0e0e0;
  --c-comment: #999999;
  --c-error: #f2777a;
  --c-value: #f99157;
  --c-symbol: #ffcc66;
  --c-string: #99cc99;
  --c-operator: #66cccc;
  --c-builtin: #70a7df;
  --c-keyword: #cc99cc;
}
 
.terminal, .terminal > .code {
  color: var(--c-syntax);
  background: var(--c-bg);
  border: 0.4rem solid var(--c-comment);
  border-radius: 1rem;
}

Debug mode

Draw lines around anything inside .debug-mode. The colour of the lines is red but defers to CSS variable --debug-colour.

You can also add div.debug-info.over and div.debug-info.under inside an element to annotate the debug boxes — though you'll need to make sure to leave enough vertical space that the annotation doesn't overlap the thing above or below it.

…like this!

.debug-mode, .debug-mode *, .debug-mode *::before, .debug-mode *::after {
  outline: 1px solid var(--debug-colour, red);
  position: relative;
}
.debug-info {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  font-family: 'Fira Code', monospace;
  font-size: 1rem;
  white-space: nowrap;
}
.debug-info.over { top: -2.5rem; }
.debug-info.under { bottom: -2.5rem; }
.debug-info p { margin: 0; }

What this is

An image carousel component that, instead of showing just one image, cycles through many.

Usage

Wherever you want your Image Carousel, slap in this code instead of your standard image block.

Make sure that any images you want are already uploaded to your page. If you chose to ignore that sage advice and only upload your images after setting up the Carousel, then your images won't work until you Hard Refresh the page (Ctrl+Shift+R).

[[include :scp-wiki:component:carousel
| images=photograph.png,old-map.png,jumpscare.gif
| caption=A selection of images.
| interval=5
| wiki=scp-wiki
| page=SCP-173
| width=300px
| height=240px
| position=right
| no-caption=false
| background=white
| options=yes
]]

Using an image carousel presents certain accessibility concerns - the constant motion can be very distracting for some users. It is also not a great idea to force your reader to wait to see your article's images. This component is also a great way to irritate your reader by forcing interactivity onto them in an otherwise non-interactive context. If possible, I would recommend not using this component. Use a series of standard image blocks instead.

What each option is

Anything in italics is optional. Everything else you gotta have.

If you omit an optional option, then it will have its default value. If you omit a non-optional option, then don't expect the carousel to work properly.

images: A list of images, separated by commas. Be sure that each one of these images is uploaded to the page. You can also specify images by URL.
These images will appear in the carousel in the order that you list them.
links:
(Optional)
caption:
(Optional)
The caption that goes underneath the carousel.
If you have no caption, make sure you set no-caption to true.
Default value: "{$caption}"
interval:
(Optional)
If you set this to a non-zero number, then the carousel will automatically move onto the next image after this number of seconds.
If the user has clicked an arrow to manually change the image, or if they are currently hovering their mouse over the carousel, then the image will not rotate.
Default value: "0"
wiki: The name of the wiki that the page that you want the carousel to be on, is on. For example, scp-wiki (NOT "scpwiki") or scp-sandbox-3.
page: The slug of the page that you want the carousel to be on. Include the category, if any. (The 'slug' is the name of the page as seen in the URL - for example, for SCP-\̅\̅\̅\̅-J the slug is scp-botnik-j.)
width:
(Optional)
The width of the widest image in the carousel.
Default value: "300px"
height:
(Optional)
The height of the tallest image in the carousel.
Default value depends on browser
position:
(Optional)
The horizontal position of the carousel on the page. "left", "right" or "center".
Default value: "right"
no-caption:
(Optional)
Set this to "true" if you don't want a caption. Otherwise, leave it blank , or set it to "false", or get rid of it completely.
Default value: "false"
background:
(Optional)
The background colour behind the images.
Default value: "transparent"
options:
(Optional)
Do you want the detailed options (play/pause button and the row of little circles) to display? If not, set to anything but "yes".
Default value: "yes"

I want the carousel to spread across the whole page!
Set width to "100%" and position to "center".

I set width/height to the size of my biggest image but it's way too big!
Pick a smaller number, or make your images smaller.



Codebase

HTML structure of the carousel

<html ng-app="carousel" ng-controller="CarouselController">
<head>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.7.2/angular.min.js"></script>
  <script src="https://scp-wiki.wikidot.com/local--code/component%3Acarousel/2"></script>
  <link href="https://d3g0gp89917ko0.cloudfront.net/v--edac79f846ba/common--theme/base/css/style.css" rel="stylesheet">
  <link href="https://scp-wiki.wdfiles.com/local--code/component%3Atheme/1" rel="stylesheet">
  <link href="https://scp-wiki.wdfiles.com/local--code/component%3Acarousel/4" rel="stylesheet">
</head>
<body>
  <div class="wrapper" id="background">
    <div class="carousel">
      <div class="horsie" ng-repeat="image in images track by $index"
           ng-class="[index > $index ? 'past' : null,
                      index === $index ? 'present' : null,
                      index < $index ? 'future' : null]">
        <img ng-src="{{image}}" ng-if="!links[$index]">
        <a href="{{links[$index]}}" target="_blank" ng-if="links[$index]">
          <img ng-src="{{image}}">
        </a>
      </div>
    </div>
    <div class="arrow decrementor"
         ng-class="index === 0 ? 'inactive' : 'active'"
         ng-click="increment(-1)">
      <div class="image"></div>
    </div>
    <div class="arrow incrementor"
         ng-class="index === images.length-1 ? 'inactive' : 'active'"
         ng-click="increment(1)">
      <div class="image"></div>
    </div>
    <div class="bubble-holder" ng-class="[options === 'yes' ? null : 'invisible']">
      <div class="bubble" ng-repeat="image in images track by $index"
           ng-class="[index === $index ? 'present' : null]"
           ng-click="selectImage($index)">
      </div>
    </div>
    <div class="control play" ng-click="control('play')"
         ng-class="[state === 'play' ? 'active' : null,
                    options === 'yes' ? null : 'invisible']"></div>
    <div class="control pause" ng-click="control('pause')"
         ng-class="[state === 'pause' ? 'active' : null,
                    options === 'yes' ? null : 'invisible']"></div>
  </div>
</body>
</html>

JS to operate the carousel

function getQueryVariable(variable) {
  var query = document.location.href.match(/\?.*$/g)[0].substring(1);
  var vars = query.split("&");
  for(var i = 0; i < vars.length; i++) { // >
    var pair = vars[i].split("=");
    if(pair[0] === variable) return pair[1]; 
  }
  return false;
}
 
(function(){
  var carousel = angular
    .module('carousel',[])
    .controller('CarouselController',CarouselController);
 
  CarouselController.$inject = ['$scope','$timeout'];
  function CarouselController($scope,$timeout){
    var wiki = getQueryVariable("wiki") || "scp-wiki";
    var page = getQueryVariable("page");
 
    var style = getQueryVariable("style");
    if (style !== "{$style}") {
      var styleElement = document.createElement("style");
      document.head.appendChild(styleElement);
      styleElement.appendChild(document.createTextNode(atob(style)));
    }
 
    var images = getQueryVariable("images").split(",");
    $scope.images = images.map(image => {
      if (!/^https?:/.test(image)) {
        // Make a full URL for the specified page
        return `https://${wiki}.wdfiles.com/local--files/${page}/${image}`;
      }
      return image;
    })
 
    var links = getQueryVariable("links");
    $scope.links = links !== "{$links}" ? links.split(",") : [];
 
    $scope.index = 0;
    $scope.increment = function(amount) {
      if(amount > 0 && $scope.index < $scope.images.length-1) { 
        $scope.index += amount;
      }
      if(amount < 0 && $scope.index > 0) { 
        $scope.index += amount;
      }
      $scope.state = "pause";
    }
 
    var interval = getQueryVariable("interval") || 0;
    if(interval === "{$interval}") interval = 0;
    $scope.state = "play";
    if(interval === 0) $scope.state = "pause";
    function oscillate() {
      $timeout(function() {
        if(!mouseover && $scope.state === "play") {
          if($scope.index < $scope.images.length-1) {
            $scope.index++;
          } else {
            $scope.index = 0;
          }
        }
        if($scope.state === "play") {
          oscillate();
        }
      }, interval*1000, true);
    }
 
    var mouseover = false;
    document.documentElement.onmouseover = function () {
      mouseover = true;
    }
    document.documentElement.onmouseout = function () {
      mouseover = false;
    }
 
    if($scope.state === "play") {
      oscillate();
    }
    document.getElementById('background').style.background = getQueryVariable("background");
 
    $scope.selectImage = function(index) {
      $scope.index = index;
      $scope.state = "pause";
    }
 
    $scope.control = function(direction) {
      switch(direction) {
        case "play":
          $scope.state = "play";
          oscillate();
          break;
        case "pause":
          $scope.state = "pause";
          break;
      }
    }
 
    $scope.options = getQueryVariable("options");
    if($scope.options === "{$options}") $scope.options = "yes";
  }
})();

Styling for the caption box

.carousel-container { position: relative; z-index: 1; float: right; margin: 0 1em 1em 1em; }
.carousel-container.left { float: left; }
.carousel-container.center { float: none; clear: both; margin: 0 auto 1em auto; }
.carousel-container iframe { position: relative; z-index: 2; width: 334px; border: none; }
.carousel-container .carousel-caption { position: relative; background-color: #eee; border: solid 1px #666; padding: 2px 0; font-size: 80%; font-weight: bold; text-align: center; width: 300px; margin-top: -3px; box-shadow: 0 1px 6px rgba(0,0,0,.25); z-index: 3; max-width: 675px; }
 
#page-content .carousel-container .carousel-caption-wrapper { max-width: 673px; }
 
.carousel-container .carousel-caption-wrapper { padding: 0 16px; }
.carousel-container .carousel-caption p { margin: 0; padding: 0 10px; }
.carousel-container .carousel-caption.true { display: none; }

Styling for the rest of the carousel

html { width: calc(100% - 32px); height: calc(100% - 4px); margin: 0; padding: 0; }
body { width: 100%; height: 100%; margin: 0; padding: 2px 16px; background: transparent; }
.wrapper { position: relative; width: 100%; height: 100%; }
.carousel { position: relative; width: calc(100% - 2px); height: calc(100% - 2px); overflow-x: hidden; border: 1px solid #666; box-shadow: 0 1px 6px rgba(0,0,0,.25); box-sizing: content-box; }
.horsie { position: absolute; height: 100%; width: 100%; top: 0; left: 0; transform: translate(0,0); transition: transform 0.3s ease-in-out; }
.horsie img { object-fit: contain; width: 100%; height: 100%; }
.horsie.past { transform: translate(-100%,0); }
.horsie.future { transform: translate(100%,0); }
.arrow { height: 30px; width: 30px; border: 1px solid #666; border-radius: 50%; position: absolute; top: 50%; background: #eee; box-shadow: 0 1px 6px rgba(0,0,0,.25); cursor: pointer; }
.arrow .image { height: 30px; width: 30px;position: absolute; top: 0; left: 0; background-position: 50% 50%; background-size: 80% 80%; background-repeat: no-repeat; opacity: 0.6; transition: opacity 0.1s ease-in-out; }
.arrow.inactive { cursor: default; }
.arrow.inactive .image { opacity: 0; }
.decrementor { left: 0; transform: translate(-50%,-50%); }
.decrementor .image { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAQAAABLCVATAAAAdElEQVR4AezQJwKAMBQD0DCOjAfD9j1BXVUdR0OxkWzCJvHvD/x5aVxEDMZBWVfAIDB1JQwOY96IUSzGYjCaxdgMJuMwOQJ4c51jlnc0HgsCUs5pPbX82csozaQsFqVuSpksSjIpg0OJBdACqlorw7AEowAAblWUrl8sD5AAAAAASUVORK5CYII='); }
.incrementor { right: 0; transform: translate(50%,-50%); }
.incrementor .image { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAQAAABLCVATAAAAcUlEQVR4Ae3QpwJAUACF4WM+sU6xV9M0TdI8mmR389j+07878PfSHOjYnYAETTdtP5Sh4VAik8pZlMSkChYlM6mSRalLKWN2FqolVLNqGgsyOE/zOZ+9i5FZTMFgJBaT34gRWUzGYAABKYMZizvm75W1TreU8DMmtioAAAAASUVORK5CYII='); }
.bubble-holder { display: flex; position: absolute; width: calc(100% - 60px); height: 20%; justify-content: center; align-items: flex-end; flex-wrap: wrap; align-content: flex-end; bottom: 0; margin: 5px 0; left: 30px; }
.bubble { border: 2px solid #666; height: 0; width: 0; margin: 5px; border-radius: 50%; transition: all 0.2s ease-in-out; background-color: white; }
.bubble.present { height: 2px; width: 2px; background-color: white; margin: 4px; }
.bubble-holder:hover .bubble { height: 6px; width: 6px; margin: 2px; cursor: pointer; }
.bubble-holder:hover .bubble.present { height: 12px; width: 12px; margin: -1px; }
.control { position: absolute; height: 10px; width: 10px; left: 5px; bottom: 5px; background-size: contain; opacity: 0.3; cursor: pointer; }
.control.active { opacity: 1; cursor: default; }
.control.play { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAQAAABLCVATAAAAgElEQVR4Ae2UQRWAIBBEJwIRjGAEI9hEG0ATaaINtIE0kAbqnrh4/M/Tzr//B7A78nzF0wvKraiAiF5ODYzIWBQIkXFpZETGqo4R2bkmRmTs6hmRERUIURsKQNSGAhBlQlSYqyXisQ/i+6tmYiA3YkUqs7SZqJHCFFtS+Lf8PZ4HilOsPBFiYmoAAAAASUVORK5CYII='); }
.control.pause { left: 20px; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAAOUlEQVR4Ae3WoQ0AMAwDwY7ezdsFzKyg3EthBgdzJCl309X7opev3wMBAQEtAQEBAQEBAU09+ZL0AdrO+xGDE6h0AAAAAElFTkSuQmCC'); }
.invisible { display: none; }
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License