[
  {
    "path": ".gitignore",
    "content": "out/*\n\n*.jpg\n*.png\n*.autosave\ninstruction.html\ninstruction.txt\n\n!doc/*.png\n"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    {\n      \"label\": \"Run Sketch\",\n      \"type\": \"shell\",\n      \"group\": {\n        \"kind\": \"build\",\n        \"isDefault\": true\n      },\n      \"command\": \"${config:processing.path}\",\n      \"presentation\": {\n        \"echo\": true,\n        \"reveal\": \"always\",\n        \"focus\": false,\n        \"panel\": \"dedicated\"\n      },\n      \"args\": [\n\t\t\"--force\",\n\t\t\"--sketch=${workspaceRoot}\",\n\t\t\"--output=${workspaceRoot}/out\",\n\t\t\"--run\"\n\t  ],\n      \"windows\": {\n        \"args\": [\n          \"--force\",\n          {\n            \"value\": \"--sketch=${workspaceRoot}\",\n            \"quoting\": \"strong\"\n          },\n          {\n            \"value\": \"--output=${workspaceRoot}\\\\out\",\n            \"quoting\": \"strong\"\n          },\n          \"--run\"\n        ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 Christian Siegel\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# knitter\n\nknitter is an open software to generate a circular/rectangular knitting pattern from a picture.\n \nThe method is inspired by the work of [Petros Vrellis](http://artof01.com/vrellis/works/knit.html).\n\n## Uses and Derivatives\n\n* [Mona Lisa](https://youtu.be/Gx26zk3MpWo) by [Anwer Al-Chalabi](https://www.youtube.com/channel/UCHSDv-MMYOPMMpnS9q8XsCA)\n* [Dog portrait](https://imgur.com/gallery/pN5T9) by [Wtfacoconut](https://imgur.com/user/Wtfacoconut)\n* Similar [algorithm using Qt/C++](https://github.com/MaloDrougard/knit) by [MaloDrougard](https://github.com/MaloDrougard) (also includes an interesting [report](https://github.com/MaloDrougard/knit/blob/master/Doc/knit-final-report.pdf) on the topic)\n* [ArtRapid](https://artrapid.com/) web app and store. Can create circular knitting patterns for free and purchase physical artworks.\n* [Stringading](https://stringading.shop/) shop. German manufacturer offering free preview and purchase of physical artworks.\n\n# How to use it\n\n1. Clone the repository (or just download [knitter.pde](https://raw.githubusercontent.com/christiansiegel/knitter/master/knitter.pde))\n2. Copy your grayscale image into the same folder as the `knitter.pde` and name it `image.jpg`. Make sure your image is square if you want to use modes *CIRCLE* or *SQUARE*, otherwise your picture will be distorted.\n3. Open `knitter.pde` with the [Processing IDE](https://processing.org/).\n4. Modify the configuration parameters at the top of the file (optional). You can also choose between *CIRCLE*, *SQUARE* and *RECTANGLE* mode here.\n5. Run Sketch.\n6. Find the best parameters using the sliders.\n\n# Output\n\n## Visual Preview\n\nWhile running the Sketch, a simulated result is shown in the window.\n\n## Instructions\n\nThe knitting order is saved to `instruction.txt`. \n\n```\nString #1454 -> next pin: 84\nString #1455 -> next pin: 122\nString #1456 -> next pin: 154\nString #1457 -> next pin: 128\nString #1458 -> next pin: 80\nString #1459 -> next pin: 14\nString #1460 -> next pin: 83\n```\n\nFurthermore, an interactive HTML page displaying and reading the single steps is generated and saved to `instruction.html`.\n\nThe pins are numbered counter-clockwise starting from 0:\n\n![Numbering](doc/numbering.png \"Numbering\")\n\n## Thread Length\n\nTo have an estimate how much thread is needed, the total length is printed to the console in the end.\n\n```\nTotal thread length: 1543 m\n```\n\n# Screenshot\n\nAn example result of running the current algorithm: \n\n![Example](doc/example.png \"Example\")\n"
  },
  {
    "path": "knitter.pde",
    "content": "import java.util.HashSet;\n\n////////////////////////////////////////////////////////////////////////////////\n// CONFIGURATION\n////////////////////////////////////////////////////////////////////////////////\n\n// Image's filename. Should be a square image for circle and square mode.\nfinal String FILENAME = \"image.jpg\";\n\n// Number of pins\nfinal int NR_PINS = 200;\n\n// RECTANGLE, SQUARE or CIRCLE shape\nfinal Mode MODE = Mode.CIRCLE;\n\n// Real size to calculate total thread length (circle diameter or square/recangle longest side)\nfinal float REAL_SIZE = 0.8; // [m]\n\n// Increase to visualize a thicker thread\nfinal float THREAD_THICKNESS = 1.0; // (1.0 = all-purpose sewing thread, approx. 0.25 mm in diameter)\n\n////////////////////////////////////////////////////////////////////////////////\n// DEFAULT VALUES\n////////////////////////////////////////////////////////////////////////////////\n\n// Default number of strings used\nfinal int DEFAULT_STRINGS = 3000;\n\n// Default color value lines are darkened if a string runs through\nfinal int DEFAULT_FADE = 25;\n\n// Default minimal distance between two consecutive pins (only for CIRCLE mode)\nfinal int DEFAULT_MIN_DIST = 25;\n\n// Default value specifying how much the drawn lines vary from a straight \n// line (preventing a moiré effect) \nfinal int DEFAULT_LINE_VARATION = 3;\n\n// Default opacity of drawn lines.\nfinal int DEFAULT_OPACITY = 50;\n\n////////////////////////////////////////////////////////////////////////////////\n// CLASSES\n////////////////////////////////////////////////////////////////////////////////\n\n// Simple point class storing x and y coordinates of a point.\nstatic class Point {\n  final int x;\n  final int y;\n\n  private Point(int x, int y) {\n    this.x = x;\n    this.y = y;\n  }\n  \n  static Point of(int x, int y) {\n    return new Point(x, y);\n  }\n}\n\n// Simple slider control for integer values.\nclass Slider {\n  private final int x, y, w, h, min, max;\n  private final String text;\n  private int value;\n\n  Slider(int x, int y, int w, int h, int value, int min, int max, String text) {\n    SLIDERS.add(this);\n    this.x = x;\n    this.y = y;\n    this.w = w;\n    this.h = h;\n    this.min = min;\n    this.max = max;\n    this.value = value;\n    this.text = text;\n  }\n  \n  // Set slider value and redraw.\n  void setValue(int value) {\n    this.value = value;\n    drawSelf();\n  }\n\n  // Draw slider \n  void drawSelf() {\n    noStroke();\n    \n    // Dark blue background\n    fill(0, 43, 91);\n    rect(x, y, w, h, h);\n\n    // Light blue slider value\n    fill(0, 113, 220);\n    float v = (float)abs(value - min) / abs(max - min);\n    rect(x, y, w * v, h, h);\n\n    // White text\n    fill(255);\n    textAlign(LEFT, TOP);\n    textSize(h - 2);\n    text(text + \": \" + value, x + h / 2, y);\n  }\n\n  // Check if mouse is pressed on slider and update value accordingly.\n  // True is returned if value was changed.\n  boolean handleMousePressed() {\n    if (mouseX >= x && mouseX <= x + w && mouseY >= y && mouseY <= y + h) {\n      float v = (float)(mouseX - x) / w;\n      int newValue = round(min + abs(max - min) * v);\n      if(newValue != value) {\n        value = newValue;\n        return true;\n      }\n    }\n    return false;\n  }\n}\n\nenum Mode {\n  CIRCLE,\n  SQUARE,\n  RECTANGLE\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// GENERIC FUNCTIONS\n////////////////////////////////////////////////////////////////////////////////\n\n// Crops image to a circular shape.\nvoid cropImageCircle(PImage image) {\n  final color white = color(255);\n  final Point center = Point.of(round(image.width / 2.0), \n                                round(image.height / 2.0));\n  final int radius = min(center.x, center.y);\n  for (int i = 0; i < image.width; i++) {\n    for (int j = 0; j < image.height; j++) {\n      if (pow(center.x - i, 2) + pow(center.y - j, 2) > pow(radius, 2)) {\n        image.set(i, j, white);\n      }\n    }\n  }\n}\n\n// Returns the coordinates of the circular/square/rectangle pins based on their number\n// and the size of the cirlce, square or rectangle.\nArrayList<Point> calcPins(int number, int w, int h, Mode mode) {\n  ArrayList<Point> pins = new ArrayList<Point>();\n  if (mode == Mode.CIRCLE) {\n    assert w == h;\n    final int size = w;\n    final float radius = size / 2.0;\n    final float angle = PI * 2.0 / number;\n    for (int i = 0; i < number; ++i) {\n      pins.add(Point.of(round(radius + radius * sin(i * angle)),\n                        round(radius + radius * cos(i * angle))));\n    }\n  } else { // SQUARE / RECTANGLE\n    int xPins = (number * w) / (2 * (h + w));\n    int yPins = (number - (2 * xPins)) / 2;\n    float spaceX = (float)w / xPins;\n    float spaceY = (float)h / yPins;\n    // top left -> bottom left\n    for (int i = 0; i < yPins; ++i) {\n      pins.add(Point.of(0, round(spaceY * i)));\n    }\n    // bottom left -> bottom right\n    for (int i = 0; i < xPins; ++i) {\n      pins.add(Point.of(round(spaceX * i), h));\n    }\n    // bottom right -> top right\n    for (int i = 0; i < yPins; ++i) {\n      pins.add(Point.of(w, h - round(spaceY * i)));\n    }\n    // top right -> top left\n    for (int i = 0; i < xPins; ++i) {\n      pins.add(Point.of(w - round(spaceX * i), 0));\n    }\n  }\n  minX = minY = 999999;\n  maxX = maxY = -1;\n  for (Point p : pins) {\n    minX = min(minX, p.x);\n    minY = min(minY, p.y);\n    maxX = max(maxX, p.x);\n    maxY = max(maxY, p.y);\n  }\n  return pins;\n}\n\n// Returns vector of pixels a line from a to b passes through.\nArrayList<Point> linePixels(Point a, Point b) {\n  ArrayList<Point> points = new ArrayList<Point>();\n  final int dx = abs(b.x - a.x);\n  final int dy = -abs(b.y - a.y);\n  final int sx = a.x < b.x ? 1 : -1;\n  final int sy = a.y < b.y ? 1 : -1;\n  int e = dx + dy, e2;\n  int px = a.x;\n  int py = a.y;\n  while (true) {\n    points.add(Point.of(px, py));\n    if (px == b.x && py == b.y) break;\n    e2 = 2 * e;\n    if (e2 > dy) {\n      e += dy;\n      px += sx;\n    }\n    if (e2 < dx) {\n      e += dx;\n      py += sy;\n    }\n  }\n  return points;\n}\n\n// Returns the score of a line from a to b, based on the image's pixels it\n// passes through (linear; black pixel gets maximum score of 255).\ndouble lineScore(PImage image, ArrayList<Point> points) {\n  int score = 0;\n  for (Point p : points) {\n    color c = image.get(p.x, p.y) & 0xff;\n    score += 0xff - c;\n  }\n  return (double)score / points.size();\n}\n\n// Reduce darkness of image's pixels the line from a to b passes through by \n// a given value (0 - 255);\nvoid reduceLine(PImage image, ArrayList<Point> points, int value) {\n  for (Point p : points) {\n    int c = image.get(p.x, p.y) & 0xff;\n    c += value;\n    if (c > 0xff) c = 0xff;\n    image.set(p.x, p.y, color(c));\n  }\n}\n\n// Returns a unique pin pair key independent of pin order.\n// This can be used as key in a map storing the lines between all\n// pins in a non-redundant manner.\nint pinPair(int a, int b) {\n  return a < b ? (10000 * a) + b : a + (10000 * b);\n}\n\n// Returns the next pin, so that the string from the current pin achieves the\n// maximum score. To prevent a string path from beeing used twice, a list of \n// already used pin pairs can be given. The minimum distance between to \n// consecutive pins is specified by minDistance. If no valid next pin can be \n// found -1 is returned.\nint nextPin(int current, HashMap<Integer, ArrayList<Point>> lines,\n            HashSet<Integer> used, PImage image, int minDistance) {\n  double maxScore = 0;\n  int next = -1;\n  for (int i = 0; i < pins.size(); ++i) {\n    if (current == i) continue;\n    int pair = pinPair(current, i);\n    \n    if (MODE == Mode.CIRCLE) {\n      // Prevent two consecutive pins with less than minimal distance\n      int diff = abs(current - i);\n      float dist = random(minDistance * 2/3, minDistance * 4/3);\n      if (diff < dist || diff > pins.size() - dist) continue;\n    } else { // SQUARE / RECTANGLE\n      // Prevent two consecutive pins on the same side\n      Point pCurr = pins.get(current);\n      Point pNext = pins.get(i);\n      if (pCurr.x == minX && pNext.x == minX) continue;\n      if (pCurr.x == maxX && pNext.x == maxX) continue;\n      if (pCurr.y == minY && pNext.y == minY) continue;\n      if (pCurr.y == maxY && pNext.y == maxY) continue;\n    }\n  \n    // Prevent usage of already used pin pair\n    if (used.contains(pair)) continue;\n\n    // Calculate line score and save next pin with maximum score\n    double score = lineScore(image, lines.get(pair));\n    if (score > maxScore) {\n      maxScore = score;\n      next = i;\n    }\n  }\n  return next;\n}\n\n// Calculates total thread length based on steps\nint totalThreadLength(IntList steps) {\n  double len = 0;\n  for (int i = 0; i < steps.size() - 1; i++) {\n    // Get pin pair\n    Point a = pins.get(steps.get(i));\n    Point b = pins.get(steps.get(i + 1));\n    // Calculate distance and add to total length\n    int x = a.x - b.x;\n    int y = a.y - b.y;\n    len += sqrt(x*x + y*y);\n   }\n   return round((float)((len * REAL_SIZE) / SIZE));\n}\n\nvoid saveInstructions(String filename, IntList steps) {\n  String html = \"<!DOCTYPE html><html> <head> <meta content=\\\"text/html;chars\" + \n                \"et=utf-8\\\" http-equiv=\\\"Content-Type\\\"/> <style>*{box-sizing\" + \n                \": border-box;}body{-webkit-touch-callout: none; -webkit-user\" + \n                \"-select: none; -khtml-user-select: none; -moz-user-select: n\" + \n                \"one; -ms-user-select: none; user-select: none;}div{text-alig\" + \n                \"n: center; font-family: sans-serif; line-height: 150%; text-\" + \n                \"shadow: 0 2px 2px #b6701e; height: 100%; color: #fff;}p{font\" + \n                \"-size: 4vw;}input{width: 100%; text-align: center;}.pin{posi\" + \n                \"tion: absolute; top: 50%; left: 50%; transform: translate(-5\" + \n                \"0%, -50%); width: 100%; padding: 20px; font-size: 16vw;}.con\" + \n                \"tainer{display: table; width: 100%;}.left-half{background-co\" + \n                \"lor: #0071DC; position: absolute; left: 0px; width: 50%;}.ri\" + \n                \"ght-half{background-color: #002B5B; position: absolute; righ\" + \n                \"t: 0px; width: 50%;}#step-input{font-size: 3vw;}</style> <sc\" + \n                \"ript src=\\\"https://ajax.googleapis.com/ajax/libs/jquery/3.1.\" + \n                \"1/jquery.min.js\\\"> </script> <script src='https://code.respo\" + \n                \"nsivevoice.org/responsivevoice.js'></script> <script type=\\\"\" + \n                \"text/javascript\\\">var stepList=[\\\"start\\\",0,\\\"end\\\"]; var cu\" + \n                \"rrent=0; function previous(){if (current > 0){current--; sho\" + \n                \"wStep();}}function next(){if (current < stepList.length - 2)\" + \n                \"{current++; showStep();}}function showStep(){$(\\\"#from-pin\\\"\" + \n                \").text(stepList[current]); $(\\\"#to-pin\\\").text(stepList[curr\" + \n                \"ent + 1]); $(\\\"#step-input\\\").val(current); responsiveVoice.\" + \n                \"speak((stepList[current + 1]).toString());}function jumpToSt\" + \n                \"ep(){current=parseInt($(\\\"#step-input\\\").val()); if (current\" + \n                \" < 0) current=0; else if (current > stepList.length - 2) cur\" + \n                \"rent=stepList.length - 2; showStep();}</script> <title>knitt\" + \n                \"er</title> </head> <body onload=\\\"showStep()\\\"> <section cla\" + \n                \"ss=\\\"container\\\"> <input id=\\\"step-input\\\" onchange=\\\"jumpTo\" + \n                \"Step()\\\" type=\\\"tel\\\"> <div class=\\\"left-half\\\" onclick=\\\"pr\" + \n                \"evious()\\\"> <p>from</p><span class=\\\"pin\\\" id=\\\"from-pin\\\">?\" + \n                \"??</span> </div><div class=\\\"right-half\\\" onclick=\\\"next()\\\"\" + \n                \"> <p>to</p><span class=\\\"pin\\\" id=\\\"to-pin\\\">???</span> </di\" + \n                \"v></section> </body></html>\";\n  String list = \",\";\n  for (int i = 0; i < steps.size(); i++) {\n    list += steps.get(i) + \",\";\n  }\n  html = html.replace(\",0,\", list);\n  saveBytes(filename, html.getBytes());\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// GLOBAL VARIABLES\n////////////////////////////////////////////////////////////////////////////////\n\n// Size of image in pixels (DO NOT CHANGE!)\nfinal int SIZE = 700;\n\n// Original image\nPImage img;\n\n// List of pin coordinates\nArrayList<Point> pins;\n\n// Min/max pin coordinates\nint minX, minY, maxX, maxY; \n\n// List of all possible lines (keys are generated by pinPair())\nHashMap<Integer, ArrayList<Point>> lines;\n\n// List of steps that generate the pattern\nIntList steps;\n\n// Slider specifying the number of strings used\nSlider stringSlider;\n\n// Slider specifying the color value lines are darkened if a string runs through\nSlider fadeSlider;\n\n// Slider specifying the how much drawn lines vary from a straight line (preventing \n// a moiré effect)\nSlider lineVariationSlider;\n\n// Slider specifying the opacity of drawn lines.\nSlider opacitySlider;\n\n// Slider specifying the minimal distance between two consecutive pins\nSlider minDistanceSlider;\n\n// Causes string pattern to be redrawn on next draw()\nboolean redraw = true;\n\nfinal HashSet<Slider> SLIDERS = new HashSet<Slider>();\n\n////////////////////////////////////////////////////////////////////////////////\n// FUNCTIONS USING GLOBAL VARIABLES\n////////////////////////////////////////////////////////////////////////////////\n\n// Clear area were strings were drawn\nvoid clearStrings() {\n  noStroke();\n  fill(255);\n  rect(0, 0, SIZE, SIZE);\n}\n\nvoid drawPins() {\n  noStroke();\n  fill(0);\n  rectMode(CENTER);\n  for (Point p : pins) {\n    rect(p.x, p.y, 2, 2);\n  }\n  rectMode(CORNER);\n}\n\nvoid drawStrings() {\n  stroke(0, opacitySlider.value);\n  strokeWeight(THREAD_THICKNESS);\n  noFill();\n  randomSeed(0);\n  int variation = lineVariationSlider.value;\n  for (int i = 0; i < steps.size() - 1; i++) {\n    // Get pin pair\n    Point a = pins.get(steps.get(i));\n    Point b = pins.get(steps.get(i + 1));\n    // Generate third point to introduce line variation (bezier control point)\n    Point c = Point.of(round(random(-variation, variation) + (a.x + b.x) / 2),\n                       round(random(-variation, variation) + (a.y + b.y) / 2));\n    // Draw string as bezier curve\n    bezier(a.x, a.y, c.x, c.y, c.x, c.y, b.x, b.y);\n  }\n}\n\nvoid drawPinHint() {\n  int topLeft, topRight, bottomLeft, bottomRight;\n  topLeft = topRight = bottomLeft = bottomRight = 0;\n  int i = 0;\n  for (Point p : pins) {\n    if (p.x == minX && p.y == minY) topLeft = i;\n    if (p.x == minX && p.y == maxY) bottomLeft = i;\n    if (p.x == maxX && p.y == maxY) bottomRight = i;\n    if (p.x == maxX && p.y == minY) topRight = i;\n    i++;\n  }\n  noStroke();\n  fill(255, 0, 0);\n  int hintSize = 10;\n  rectMode(CENTER);\n  rect(minX, minY, hintSize, hintSize);\n  rect(minX, maxY, hintSize, hintSize);\n  rect(maxX, maxY, hintSize, hintSize);\n  rect(maxX, minY, hintSize, hintSize);\n  rectMode(CORNER);\n  textSize(30);\n  textAlign(LEFT, TOP);\n  whiteOutlinedRedText(\"#\" + topLeft, minX + 18, minY + 18);\n  textAlign(LEFT, BOTTOM);\n  whiteOutlinedRedText(\"#\" + bottomLeft, minX + 18, maxY - 18);\n  textAlign(RIGHT, BOTTOM);\n  whiteOutlinedRedText(\"#\" + bottomRight, maxX - 18, maxY - 18);\n  textAlign(RIGHT, TOP);\n  whiteOutlinedRedText(\"#\" + topRight, maxX - 18, minY + 18);\n  fill(0);\n  textAlign(LEFT, TOP);\n  textSize(16);\n  text(\"Total pins: \" + pins.size(), 10, height - 22);\n}\n\nvoid whiteOutlinedRedText(String s, int x, int y) {\n  fill(255);\n  for(int i = -1; i < 2; i++){\n    text(s, x + i, y);\n    text(s, x, y + i);\n  }\n  fill(255, 0, 0);\n  text(s, x, y);\n}\n\nvoid drawPattern() {\n  pushMatrix();\n  {\n    translate(width - SIZE, 0);\n    clearStrings();\n    drawPins();\n    drawStrings();\n    if (MODE == Mode.SQUARE || MODE == Mode.RECTANGLE) {\n      drawPinHint();\n    }\n  }\n  popMatrix();\n}\n\nvoid drawSliders() {\n  for (Slider s : SLIDERS) {\n    s.drawSelf();\n  }\n}\n\n// Generate string pattern\nvoid generatePattern() {\n  steps = new IntList();\n  StringBuilder stepsInstructions = new StringBuilder();\n  \n  // Work on copy of image\n  PImage imgCopy = createImage(img.width, img.height, RGB);\n  imgCopy.copy(img, 0, 0, img.width, img.height, 0, 0, img.width, img.height);\n  \n  // Always start from pin 0\n  int current = 0;  \n  steps.append(current);\n  \n  HashSet<Integer> used = new HashSet<Integer>();\n  for (int i = 0; i < stringSlider.value; ++i) {\n    // Get next pin\n    int next = nextPin(current, lines, used, imgCopy, MODE == Mode.CIRCLE ? minDistanceSlider.value : -999);\n    if(next < 0) {\n      stringSlider.setValue(used.size());\n      break;\n    }\n    \n    // Reduce darkness in image\n    int pair = pinPair(current, next);\n    reduceLine(imgCopy, lines.get(pair), fadeSlider.value);\n\n    stepsInstructions.append(\"String #\").append(i).append(\" -> next pin: \").append(next).append(\"\\r\\n\");\n  \n    used.add(pair);\n    steps.append(next);\n    current = next;\n  }\n  \n  println(\"Total thread length: \" + totalThreadLength(steps) + \" m\");\n  \n  // Save instructions in two different formats\n  saveBytes(\"instruction.txt\", stepsInstructions.toString().getBytes());\n  saveInstructions(\"instruction.html\", steps);\n}\n\nvoid initSliders() {\n  int x = 5;\n  int w = width - 10;\n  int h = 20;\n  int y = SIZE + 10;\n  int spaceY = 25;\n  stringSlider = new Slider(x, y, w, h, DEFAULT_STRINGS, 0, 10000, \"strings\");\n  fadeSlider = new Slider(x, y += spaceY, w, h, DEFAULT_FADE, 0, 255, \"fade\");\n  if (MODE == Mode.CIRCLE) {\n    minDistanceSlider = new Slider(x, y += spaceY, w, h, DEFAULT_MIN_DIST, 0, pins.size() / 2, \"average minimal distance\");\n  }\n  lineVariationSlider = new Slider(x, y += spaceY, w, h, DEFAULT_LINE_VARATION, 0, 20, \"line variation\");\n  opacitySlider = new Slider(x, y += spaceY, w, h, DEFAULT_OPACITY, 0, 100, \"opacity\");\n}\n\nvoid setup() {\n  size(1410, 835);\n  background(255);\n  randomSeed(0);\n\n  // Load image from file and draw it\n  img = loadImage(FILENAME);\n  if (img == null) {\n    println(\"Couldn't load image file '\" + sketchFile(FILENAME) + \"'!\");\n    exit();\n    return;\n  }\n  img.filter(GRAY);\n  if (MODE == Mode.RECTANGLE) {\n    if (img.width > img.height) {\n      img.resize(SIZE, 0);\n    } else {\n      img.resize(0, SIZE);\n    }\n  } else {\n    img.resize(SIZE, SIZE);\n  }\n  if (MODE == Mode.CIRCLE) {\n    cropImageCircle(img);\n  }\n  image(img, 0, 0);\n\n  // Calculate pins\n  pins = calcPins(NR_PINS, img.width, img.height, MODE);\n  if (pins == null) {\n    exit();\n    return;\n  }\n\n  // Calculate the pixels of all possible lines\n  lines = new HashMap<Integer, ArrayList<Point>>();\n  for (int i = 0; i < pins.size(); ++i) {\n    for (int j = i + 1; j < pins.size(); ++j) {\n      lines.put(pinPair(i, j), linePixels(pins.get(i), pins.get(j)));\n    }\n  }\n\n  initSliders();\n  generatePattern();\n}\n\nvoid draw() {\n  if (redraw) {\n    drawPattern();\n    drawSliders();\n    redraw = false;\n  }\n}\n\nvoid mouseReleased() {\n  boolean generateNeeded = false;\n  generateNeeded |= stringSlider.handleMousePressed();\n  generateNeeded |= fadeSlider.handleMousePressed();\n  if (minDistanceSlider != null) {\n    generateNeeded |= minDistanceSlider.handleMousePressed();\n  }\n\n  if (generateNeeded) {\n    generatePattern();\n  }\n\n  redraw |= generateNeeded;\n  redraw |= lineVariationSlider.handleMousePressed();\n  redraw |= opacitySlider.handleMousePressed();\n}\n  \n"
  }
]