Neither one nor Many

 
June 21 2018

I remember having something like this as a child: http://www.endlessfoldingcard.com/endless-folding-card-p-768-918.html. It was not that one but something similar, a flyer for some stupid product. I was fascinated by it and could keep folding it for hours. It was nice made out of some very strong paper/cardboard, unfortunately I probably got rid of it at some point.

It took me a long time to find it again, every now and then I would try to look it up on Google (with years in between), unable to find it. Until I had a moment of clarity and tried the additional keyword "Endless", and finally found something that I remembered.

Figuring out how it works

All the YouTube videos I found would basically fold the thing together, and then decorate the card afterward, however I wanted to print it beforehand in such a way that it would turn out nicely when folded. To be honest this one does attempt to explain some of the layouting, but it wasn't clear enough for me. This is another video that shows how to fold it clearly.

There are a few things that you will notice when you fold one, some parts of the card stay constant for example, and not all views have corners. Anyway I decided to treat it as a grid of tiles. I just printed 2 two-sided pieces of paper with unique numbers for each tile. Then I deciphered which tiles end up where after folding, and which ones are rotated 180 degrees. See madness below.

     

See the cardboard in action here: VID_20180531_011204.mp4

Design and print the card!

Designing the card in something like gimp is a lot of work of course, and it would be hell if you need to prepare it manually for printing. Luckily I wrote a C++ program that uses the very cool Selene Image library, which I learned about via the Dutch C++ User Group , Michael (the author) gave an awesome lightning talk about it. It was good timing because a few days after that I needed to write this program.

I didn't put this code on github, but here is the source code:

// This file is using the `Selene` library.
// Copyright 2017-2018 Michael Hofmann (https://github.com/kmhofmann).
// Distributed under MIT license. See accompanying LICENSE file in the top-level directory.

#include <selene/img/ImageAccess.hpp>
#include <selene/img/ImageDataToImage.hpp>
#include <selene/img/ImageToImageData.hpp>
#include <selene/img_io/IO.hpp>
#include <selene/img_ops/Algorithms.hpp>
#include <selene/img_ops/Transformations.hpp>
#include <selene/io/FileReader.hpp>
#include <selene/io/FileWriter.hpp>

#include <cassert>
#include <iostream>
#include <string>

#include <boost/filesystem.hpp>

using namespace sln::literals;

struct tile
{
  size_t index;
  size_t folded_index;
  bool flipped;
};

struct tile_index
{
  sln::PixelIndex x;
  sln::PixelIndex y;
};

// clang-format off
std::array<tile, 64> tiles {{
  /**
   * There are basically four sides (a,b,c,d), which I've divided into 16 tiles per side.
   * Side a & b will be printed on one piece of paper, double sided, as will sides c & d.
   * In total there are 64 tiles, and this table basically combines two states for each tile.
   *
   * When designing you create four "views" for each side, which is the desired outcome,
   * or the first state of each tile. Like:
   * {1, 2, 3, ..., 17, 18, 19, ..., 62, 53, 64.} (nothing will be rotated)
   *
   * But once you print them and glue them together, and you fold your way through the card,
   * the tiles appear in different order, and sometimes even rotated 180 degrees.
   * This is the second state of the tiles. Something like:
   * {1, 2, 3, ..., 19, 50, 51, ..., 43+rotate, 42+rotate, 43+rotate}
   *
   * Both states are combined in this table, for each tile the following fields:
   * - index: the tile number (for the first state)
   * - folded index: the tile number (for the second state)
   * - flipped: whether the tile is rotated 180 degrees in the 2nd state.
   *
   * So basically what needs to happen is move each tile from index -> folded_index, rotate
   * if needed, and you can just print the resulting images and when you assemble the card
   * all your designs appear the way you intended them to. Doing this in something like
   * Photoshop or Gimp would be a huge pain, so that's why I made this program.
   */
  // a
  {1, 1, false},   {2, 2, false},   {3, 3, false},   {4, 4, false},
  {5, 5, false},   {6, 6, false},   {7, 7, false},   {8, 8, false},
  {9, 9, false},   {10, 10, false}, {11, 11, false}, {12, 12, false},
  {13, 13, false}, {14, 14, false}, {15, 15, false}, {16, 16, false},

  // b (corner tiles are glued)
  {17, 19, false}, {18, 50, false}, {19, 51, false}, {20, 18, false},
  {21, 23, false}, {22, 54, false}, {23, 55, false}, {24, 22, false},
  {25, 27, false}, {26, 58, false}, {27, 59, false}, {28, 26, false},
  {29, 31, false}, {30, 62, false}, {31, 63, false}, {32, 30, false},

  // c (corner tiles are unset, tiles on left and right are similar as previous (side b))
  {33, 0, false},  {34, 53, true},  {35, 56, true},  {36, 0, false},
  {37, 0, false},  {38, 24, false}, {39, 21, false}, {40, 0, false},
  {41, 0, false},  {42, 28, false}, {43, 25, false}, {44, 0, false},
  {45, 0, false},  {46, 57, true},  {47, 60, true},  {48, 0, false},

  // d (corner tiles are glued)
  {49, 40, true},  {50, 39, true},  {51, 38, true},  {52, 37, true},
  {53, 36, true},  {54, 35, true},  {55, 34, true},  {56, 33, true},
  {57, 48, true},  {58, 47, true},  {59, 46, true},  {60, 45, true},
  {61, 44, true},  {62, 43, true},  {63, 42, true},  {64, 41, true},
}};
// clang-format on

template <typename PixelType>
sln::Image<PixelType> load_image(const char* filename)
{
  auto img_data = sln::read_image(sln::FileReader(filename));
  if (!img_data.is_valid())
  {
    std::cerr << "Image data could not be decoded." << std::endl;
  }
  const auto img = sln::to_image<PixelType>(std::move(img_data));
  assert(img.is_valid());
  return img;
}

template <typename T>
void copy(T src, T dst)
{
  for (auto y = 0_idx; y < src.height(); ++y)
  {
    for (auto ptr = src.data(y), end = src.data_row_end(y), ptr2 = dst.data(y); ptr != end; ++ptr, ++ptr2)
    {
      *ptr2 = *ptr;
    }
  }
}

int main()
{
  // load the four all-equal in size views for the endless card
  auto img1 = load_image<sln::Pixel_8u4>("card_a.png");
  auto img2 = load_image<sln::Pixel_8u4>("card_b.png");
  auto img3 = load_image<sln::Pixel_8u4>("card_c.png");
  auto img4 = load_image<sln::Pixel_8u4>("card_d.png");

  assert(img1.width() == img2.width() && img1.height() == img2.height());
  assert(img2.width() == img3.width() && img2.height() == img3.height());
  assert(img3.width() == img4.width() && img3.height() == img4.height());

  // concat all images into one long image, making it easier to calculate coordinates for each tile
  sln::Image<sln::Pixel_8u4> input(img1.width(), sln::to_pixel_length(img1.height() * 4));
  sln::Image<sln::Pixel_8u4> output = clone(input);

  copy(img1, sln::view(input, 0_idx, 0_idx, img1.width(), img1.height()));
  copy(img2, sln::view(input, 0_idx, sln::to_pixel_index(img1.height() * 1), img1.width(), img1.height()));
  copy(img3, sln::view(input, 0_idx, sln::to_pixel_index(img1.height() * 2), img1.width(), img1.height()));
  copy(img4, sln::view(input, 0_idx, sln::to_pixel_index(img1.height() * 3), img1.width(), img1.height()));

  // helper lambda to get coordinate for a given tile number
  const auto tile_width = sln::to_pixel_length(img1.width() / 4);
  const auto tile_height = sln::to_pixel_length(img1.height() / 4);
  auto index_to_x_and_y = [=](size_t index) -> tile_index {
    const size_t x = ((index - 1) % 4) * static_cast<sln::PixelLength::value_type>(tile_width);
    const size_t y = ((index - 1) / 4) * static_cast<sln::PixelLength::value_type>(tile_height);
    return {sln::to_pixel_index(x), sln::to_pixel_index(y)};
  };

  // copy each tile to the correct location for printing the endless card
  for (const auto& tile : tiles)
  {
    auto src_index = index_to_x_and_y(tile.index);
    auto dst_index = index_to_x_and_y(tile.folded_index);
    if (tile.folded_index == 0)
    {
      continue;
    }
    auto src = sln::view(input, src_index.x, src_index.y, tile_width, tile_height);
    auto dst = sln::view(output, dst_index.x, dst_index.y, tile_width, tile_height);
    if (tile.flipped)
    {
      copy(sln::rotate<sln::RotationDirection::Clockwise180>(src), dst);
    }
    else
    {
      copy(src, dst);
    }
  }

  // debug
  sln::write_image(to_image_data_view(input, sln::PixelFormat::RGBA), sln::ImageFormat::PNG,
                   sln::FileWriter("result.png"));
  sln::write_image(to_image_data_view(output, sln::PixelFormat::RGBA), sln::ImageFormat::PNG,
                   sln::FileWriter("result2.png"));

  // write the resulting cards ready to print, result a + b double sided, and c + d double sided.
  sln::write_image(
      to_image_data_view(sln::view(output, 0_idx, 0_idx, img1.width(), img1.height()), sln::PixelFormat::RGBA),
      sln::ImageFormat::PNG, sln::FileWriter("result_a.png"));
  sln::write_image(
      to_image_data_view(sln::view(output, 0_idx, sln::to_pixel_index(img1.height() * 1), img1.width(), img1.height()),
                         sln::PixelFormat::RGBA),
      sln::ImageFormat::PNG, sln::FileWriter("result_b.png"));
  sln::write_image(
      to_image_data_view(sln::view(output, 0_idx, sln::to_pixel_index(img1.height() * 2), img1.width(), img1.height()),
                         sln::PixelFormat::RGBA),
      sln::ImageFormat::PNG, sln::FileWriter("result_c.png"));
  sln::write_image(
      to_image_data_view(sln::view(output, 0_idx, sln::to_pixel_index(img1.height() * 3), img1.width(), img1.height()),
                         sln::PixelFormat::RGBA),
      sln::ImageFormat::PNG, sln::FileWriter("result_d.png"));

  return 0;
}

Input & Output

The program reads card_a.png, card_b.png, card_c.png, card_d.png. Does it's magic and procudes: result_a.png, result_b.png, result_c.png, result_d.png.

<-- the *input* images <-- the *output* images

As you can see the resulting images look a bit weird, but when printed double sidedly, card A and B on one side, and card C and D on the other side of the paper, you can cut them out and fold them.

How to fold?

This is how I did it, I didn't plan to make a blog post so I didn't completely document every step. But at least on this picture should give you an idea:

For folding I bought a "Scor-Pal", I can recommend this tool it makes nice folds, doing without would give crappy results on thick paper IMO.

The polar bear piece of paper is side A & C, and cut in the middle already, and two horizontal pre-folds are made (sloppy ones though ). The other two sides C & D are pre-folded as well, but I didn't cut it horizontally yet. After cutting, glue the white edges on sides B & C together, and have it dry, the card should be done.

Conclusion

After having folded about 60 of these I finally got the hang of it and I could produce pretty slick cards. One thing I did was print myself with a color laser printer on very thick paper, this gave "meh" results, the toner had trouble sticking to the paper especially after folding. I recommend doing the printing at a specialized shop, maybe to avoid the toner letting loose, but also because aligning both sides is important. Not all printers are great at this I've discovered, especially if you have to use the manual feed for the thick paper.

What worked best is this order:

  • Printing both sides
  • Cut out the two sides with something like
  • Do the (in total four) pre-folds on both pieces of paper (do them in the right direction)
  • Cut the first one vertically, perfectly in the middle.
  • Cut the second one horizontally, perfectly in the middle.
  • Put glue on the corners for the second one now (the one cut horizontally)
  • Then align those two pieces perfectly so it appears whole again.
  • One by one add the other (the one cut vertically) two pieces, you'll find with the glue still wet it's easy to adjust.
  • When done, let it dry, and later on pre-fold some more for the definitive card.
C++ Comments (1)

Andrew Stoeckley

website: andrewstoeckley.com @

2018-06-22 18:55:53

This is one of the dopest uses of C++ I've seen in awhile!


Leave a Reply

Comment may not be visible immediately, because I process everything manually.**

**) I plan to automate this.., but it's on my ToDo since for ever..


Author:
Ray Burgemeestre
february 23th, 1984

Topics:
C++, Linux, Webdev

Other interests:
Music, Art, Zen