block by dribnet 088b24df9bc25b10055995cd83aaa3fd

MDDN242 Assignment 3: Glyph

Full Screen

PS3 MDDN 242 2019

Color Glyphs

This is example of a spinning glyph. The hue controls the spin and the saturation and lightness control the two different halves. A small dot was added as a refrence point to reduce the visual symmetry.

index.html

<head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/addons/p5.dom.js"></script>
    <script src="z_hsluv-0.0.3.min.js"  type="text/javascript"></script>
    <script language="javascript" type="text/javascript" src="z_purview_helper.js"></script>
    <script language="javascript" type="text/javascript" src="z_color_helper.js"></script>
    <script language="javascript" type="text/javascript" src="glyph.js"></script>
    <script language="javascript" type="text/javascript" src="sketch.js"></script>
    <style>
        body   { padding: 0; margin: 0; }
        .inner { position: absolute; }
        #controls {
            font: 300 12px "Helvetica Neue";
            padding: 5;
            margin: 5;
            background: #f0f0f0;
            opacity: 0.0;
            -webkit-transition: opacity 0.2s ease;
            -moz-transition: opacity 0.2s ease;
            -o-transition: opacity 0.2s ease;
            -ms-transition: opacity 0.2s ease;
        }
        #controls:hover { opacity: 0.9; }
    </style>
</head>
<body style="background-color:white">
    <div class="outer">
        <div class="inner">
            <div id="canvasContainer"></div>
        </div>
        <div class="inner" id="controls" height="500px">
            <table>
                <tr>
                    <td>Hue</td>
                    <td id="slider1Container"></td>
                </tr>
                <tr>
                    <td>Saturation</td>
                    <td id="slider2Container"></td>
                </tr>
                <tr>
                    <td>Lightness</td>
                    <td id="slider3Container"></td>
                </tr>
                <tr>
                    <td>
                        <hr>
                    </td>
                </tr>
                <tr>
                    <td>Mode</td>
                    <td id="selector1Container"></td>
                </tr>
                <tr>
                    <td>Size</td>
                    <td id="selector3Container"></td>
                </tr>
                <tr>
                    <td>Display</td>
                    <td id="selector2Container"></td>
                </tr>
                <tr>
                    <td>Show Guide</td>
                    <td id="checkContainer"></td>
                </tr>
                <tr>
                    <td></td>
                    <td id="buttonContainer"></td>
                </tr>
        </div>
    </div>
</table>
</body>

glyph.js

/* change default application behavior */
var defaultMode = "random";
var defaultSize = 128;
var defaultDisplay = "both"
var defaultEmoji = 100;
var backgroundColor = "hsb(0, 0%, 94%)";

function Glyph() {
  /*
   * values is an array of 3 numbers: [hue, saturation, lightness]
   *   + hue ranges from 0..360
   *   + saturation ranges from 0..100
   *   + lightness ranges from 0..100
   *
   * size is the number of pixels for width and height
   *
   * use p5.js to draw a round grayscale glyph
   * the glyph should stay within the ellipse [0, 0, width, height]
   * this is a grayscale glyph, so only lightness can be adjusted.
   * the range for lightness is 0..100
   *
   * When setting the lightness of stroke or fill always use either strokeUniform()
   * or fillUniform() calls. Each takes one arguement - the lightness from
   * 0 to 100. Examples:
   *       - fillUniform(50);    // ranges from 0-100
   *       - strokeUniform(100); // white
   */ 
  this.draw = function(values, size) {
    let s2 = size/2;
    translate(s2, s2, 0);

    // map lightness to large circle shade
    let spin = map(values[0], 0, 360, 0, 2*PI)
    rotate(spin)

    let bg = map(values[1], 0, 100, 50, 100);
    let fg = map(values[2], 0, 100, 50, 0);

    fillUniform(bg);
    ellipse(0, 0, size, size);
    fillUniform(0);
    ellipse(0, -(2*s2/3), s2/3, s2/3);

    fillUniform(fg);
    arc(0, 0, size, size, 0, PI);
  }  
}

purview.json

{
  "commits": [
    {
      "sha": "e5f0ddcfa4f1338f91ca6b24e22f103ff2b98637",
      "name": "example2: spinner"
    },
    {
      "sha": "2602d38da41ef93c92cb2fc1c17b2d14ad8cd087",
      "name": "example1: stripe box"
    },
    {
      "sha": "1b92f81f552ec3a3b1932bc6d67a98094c0ddd1a",
      "name": "sketch example"
    }
  ]
}

sketch.js


if (typeof defaultMode === 'undefined') {
  var defaultMode = "sketch";
}

if (typeof defaultSize === 'undefined') {
  var defaultSize = "128";
}

if (typeof defaultDisplay === 'undefined') {
  var defaultDisplay = "both";
}

if (typeof defaultEmoji === 'undefined') {
  var defaultDisplay = 76;
}

if (typeof backgroundColor === 'undefined') {
  var backgroundColor = "rgb(232, 232, 232)";
}


let canvasWidth = 960;
let canvasHeight = 500;

let glyphSelector;
let modeSelector;
let sizeSelector;
let show_oddball = false;
let oddball_row = 0;
let oddball_col = 0;

let val_sliders = [];
let max_vals = [360, 100, 100];

let curEmoji = defaultEmoji;
let NUM_EMOJI = 872;
let EMOJI_WIDTH = 38;

let lastKeyPressedTime;
let secondsUntilSwapMode = 10;
let secondsPerEmoji = 5;
let isSwappingEmoji = false;
let emojiSwapLerp = 0;
let prevEmoji = 0;
let lastEmojiSwappedTime;

let emojiImg;
let sketchImg;
let curEmojiImg;
let curEmojiPixels;
let curEmojiColors, nextEmojiColors, prevEmojiColors;
function preload() {
  emojiImg = loadImage("twemoji36b_montage.png");
  sketchImg = loadImage("sketch.png");
}

function setup() {
  // create the drawing canvas, save the canvas element
  let main_canvas = createCanvas(canvasWidth, canvasHeight);
  main_canvas.parent('canvasContainer');

  let now = millis();
  lastKeyPressedTime = now;
  lastEmojiSwappedTime = now;

  // create two sliders
  for (i=0; i<3; i++) {
    let slider = createSlider(0, 10*max_vals[i], 10*max_vals[i]/2);
    slider.parent("slider" + (i+1) + "Container")
    slider.changed(sliderUpdated);
    slider.mouseMoved(sliderUpdated);
    slider.touchMoved(sliderUpdated);
    val_sliders.push(slider);
  }

  modeSelector = createSelect();
  modeSelector.option('sketch');
  modeSelector.option('edit');
  modeSelector.option('random');
  modeSelector.option('gradient');
  modeSelector.option('oddball');
  modeSelector.option('image');
  modeSelector.changed(modeChangedEvent);
  modeSelector.value(defaultMode);
  modeSelector.parent('selector1Container');

  glyphSelector = createSelect();
  glyphSelector.option('color');
  glyphSelector.option('glyph');
  glyphSelector.option('both');
  glyphSelector.value(defaultDisplay);
  glyphSelector.parent('selector2Container');

  sizeSelector = createSelect();
  sizeSelector.option('32');
  sizeSelector.option('64');
  sizeSelector.option('128');
  sizeSelector.parent('selector3Container');
  sizeSelector.value(defaultSize);
  sizeSelector.changed(sizeChangedEvent);


  guideCheckbox = createCheckbox('', false);
  guideCheckbox.parent('checkContainer');
  guideCheckbox.changed(guideChangedEvent);

  button = createButton('redo');
  button.mousePressed(buttonPressedEvent);
  button.parent('buttonContainer');

  curEmojiImg = createImage(36, 36);
  // create an array for HSB values: [18][18][3]
  curEmojiPixels = Array(18);
  curEmojiColors = Array(18);
  for(let i=0; i<18; i++) {
    curEmojiPixels[i] = Array(18);
    curEmojiColors[i] = Array(18);
    for(let j=0; j<18; j++) {
      curEmojiPixels[i][j] = Array(3);
    }
  }

  gray_glyph = new Glyph();
  refreshGridData();
  modeChangedEvent();
}

function sliderUpdated() {
    redraw();
}

function mouseClicked() {
  if (mouseX > width/4) {
    refreshGridData();
  }
  redraw();
}

function dataInterpolate(data1, data2, val) {
  let d = new Array(3);
  for(let i=0; i<3; i++) {
    d[i] = lerp(data1[i], data2[i], val);
  }
  return d;
}

let numGridRows;
let numGridCols;
let gridValues; // row, col order
let gridOffsetX, gridOffsetY;
let gridSpacingX, gridSpacingY;
// Generate data for putting glyphs in a grid

function clamp(num, min, max) {
  return Math.min(Math.max(num, min), max);
}

function refreshGridData() {
  let mode = modeSelector.value();
  let glyphSize = parseInt(sizeSelector.value(), 10);

  if (mode == "image") {
    if(glyphSize == 32) {
      numGridCols = 18;
      numGridRows = 17;
      gridOffsetX = 320;
      gridSpacingX = 31;
      gridOffsetY = 2;
      gridSpacingY = 29;
    }
    else if(glyphSize == 64) {
      numGridCols = 10;
      numGridRows = 9;
      gridOffsetX = 280;
      gridSpacingX = 66;
      gridOffsetY = -18;
      gridSpacingY = 59;
    }
    else if(glyphSize == 128) {
      numGridCols = 6;
      numGridRows = 5;
      gridOffsetX = 164;
      gridSpacingX = 132;
      gridOffsetY = -50;
      gridSpacingY = 118;
    }
    else if(glyphSize == 256) {
      numGridCols = 3;
      numGridRows = 3;
      gridOffsetX = 172;
      gridSpacingX = 262;
      gridOffsetY = -100;
      gridSpacingY = 234;
    }
  }
  else if(glyphSize == 128) {
    numGridCols = 6;
    numGridRows = 3;
    gridOffsetX = 38;
    gridSpacingX = 156;
    gridOffsetY = 32;
    gridSpacingY = 166;
  }
  else if(glyphSize == 256) {
    numGridCols = 3;
    numGridRows = 1;
    gridOffsetX = 20;
    gridSpacingX = 320;
    gridOffsetY = 100;
    gridSpacingY = 500;
  }
  else if(glyphSize == 64) {
    numGridCols = 12;
    numGridRows = 6;
    gridOffsetX = 15;
    gridSpacingX = 78;
    gridOffsetY = 15;
    gridSpacingY = 81;
  }
  else if(glyphSize == 32) {
    numGridCols = 21;
    numGridRows = 11;
    gridOffsetX = 22;
    gridSpacingX = 44;
    gridOffsetY = 22;
    gridSpacingY = 42;
  }
  gridValues = new Array(numGridRows);
  for (let i=0; i<numGridRows; i++) {
    gridValues[i] = new Array(numGridCols);
    for (let j=0; j<numGridCols; j++) {
      gridValues[i][j] = new Array(8);
    }
  }
  if (mode == "gradient" || mode == 'oddball') {
    let top_left = Array(3);
    let top_right = Array(3);
    let bottom_left = Array(3);
    let bottom_right = Array(3);
    for (let k=0; k<3; k++) {
      top_left[k] = random(max_vals[k]);
      top_right[k] = random(max_vals[k]);
      bottom_left[k] = random(max_vals[k]);
      bottom_right[k] = random(max_vals[k]);
    }
    for (let i=0; i<numGridRows; i++) {
      let frac_down = 0;
      if(numGridRows != 1) {
        frac_down = i / (numGridRows - 1.0);
      }
      d_left = dataInterpolate(top_left, bottom_left, frac_down);
      d_right = dataInterpolate(top_right, bottom_right, frac_down);
      for (let j=0; j<numGridCols; j++) {
        let frac_over = 0;
        if(numGridCols != 0) {
          frac_over = j / (numGridCols - 1.0);
        }
        gridValues[i][j] = dataInterpolate(d_left, d_right, frac_over);
      }
    }
    if (mode == 'oddball') {
      // replace an entry at random
      oddball_row = Math.floor(random(numGridRows))
      oddball_col = Math.floor(random(numGridCols))
      for (let k=0; k<3; k++) {
        gridValues[oddball_row][oddball_col][k] = random(max_vals[k]);
      }
    }
  }
  else if(mode == "image") {
    for (let i=0; i<numGridRows; i++) {
      for (let j=0; j<numGridCols; j++) {
        for (let k=0; k<3; k++) {
          gridValues[i][j][k] = curEmojiPixels[i][j][k];
        }
      }
    }
  }
  else {
    for (let i=0; i<numGridRows; i++) {
      for (let j=0; j<numGridCols; j++) {
        for (let k=0; k<3; k++) {
          gridValues[i][j][k] = random(max_vals[k]);
        }
      }
    }
  }
}

function sizeChangedEvent() {
  let mode = modeSelector.value();
  if (mode != 'edit') {
    refreshGridData();
  }
  redraw();
}

function guideChangedEvent() {
  show_oddball = guideCheckbox.checked();
  redraw();
}

function modeChangedEvent() {
  let mode = modeSelector.value();

  // enable/disable sliders
  if (mode === 'edit') {
    // disable the button
    // button.attribute('disabled','');

    // enable the size selector
    sizeSelector.removeAttribute('disabled');

    // enable the first four sliders
    for(let i=0; i<3; i++) {
      val_sliders[i].removeAttribute('disabled');  
    }
  }
  else {
    // enable the button
    // button.removeAttribute('disabled');

    // disable the sliders
    for(let i=0; i<3; i++) {
      val_sliders[i].attribute('disabled','');
    }

    // enable the size selector
    // sizeSelector.removeAttribute('disabled');

    // refresh data
    refreshGridData();
  }
  if (mode === "image") {
    // get current emoji image
    let offsetX = 36 * (curEmoji % 38);
    let offsetY = 36 * Math.floor(curEmoji / 38);

    let squareOffsets = [ [0,0], [0,1], [1,1], [1, 0] ];
    curEmojiImg.copy(emojiImg, offsetX, offsetY, 36, 36, 0, 0, 36, 36);
    curEmojiImg.loadPixels();
    for(let i=0; i<17; i++) {
      // i is y
      let maxX = 18;
      let offsetX = 0;
      if (i%2 == 1) {
        maxX = 17;
        offsetX = 1;
      }
      for(let j=0; j<maxX; j++) {
        // j is x
        let sumColor = [0, 0, 0];
        for(let k=0; k<4; k++) {
          // k is summing over 4 adacent pixels
          let curColor = curEmojiImg.get(j*2 + squareOffsets[k][0] + offsetX, 1 + i*2 + squareOffsets[k][1]);
          for(let l=0; l<3; l++) {
            sumColor[l] += curColor[l] / 4.0;
          }
        }
        let curColor = color(sumColor);
        curEmojiColors[i][j] = curColor;
        let rgba = curColor._array;
        let rgb = [ rgba[0], rgba[1], rgba[2] ];
        let hscol = hsluv.rgbToHsluv(rgb);
        curEmojiPixels[i][j][0] = hscol[0];
        curEmojiPixels[i][j][1] = hscol[1];
        curEmojiPixels[i][j][2] = hscol[2];
      }
    }
    // refresh data
    refreshGridData();
  }

  redraw();
}

function buttonPressedEvent() {
  refreshGridData();
  redraw();
}

// function fillHsluv(h, s, l) {
//   var rgb = hsluv.hsluvToRgb([h, s, l]);
//   fill(rgb[0] * 255, rgb[1] * 255, rgb[2] * 255);
// }

function ColorGlyph() {
  this.draw = function(values, size) {
    let rgb = hsluv.hsluvToRgb(values);
    fillHsluv(values[0], values[1], values[2]);
    // print(values);
    // fill(rgb[0]*255, rgb[1]*255, rgb[2]*255);
    stroke(0);
    let s2 = size/2;
    ellipse(s2, s2, size);
  }
}
let color_glyph = new ColorGlyph();

function highlightGlyph(glyphSize) {
  let halfSize = glyphSize / 2.0;
  stroke(0, 0, 255, 128);
  noFill();
  strokeWeight(4);
  ellipse(halfSize, halfSize, glyphSize+4);
  fill(0);
  strokeWeight(1);
}

function getGyphObject() {
  return gray_glyph;
}

function drawGlyph(glyphValues, glyphSize) {
  let shawdowShift = glyphSize / 5.0;
  let glyphMode = glyphSelector.value();
   if(glyphMode != "glyph") {
    translate(-shawdowShift, -shawdowShift);
    color_glyph.draw(glyphValues, glyphSize);
    translate(shawdowShift, shawdowShift);
  }
  if(glyphMode != "color") {
    gray_glyph.draw(glyphValues, glyphSize);
  }
}

function drawDriveMode() {
  let glyphSize = 320;
  let halfSize = glyphSize / 2;

  background(backgroundColor);
  let middle_x = canvasWidth / 2;
  let middle_y = canvasHeight / 2;
  middle_x = middle_x + glyphSize / 10.0;
  middle_y = middle_y + glyphSize / 10.0;
  let val = [0,0,0];
  for(i=0; i<3; i++) {
    val[i] = val_sliders[i].value() / 10.0;
  }

  resetMatrix();
  translate(middle_x - halfSize, middle_y - halfSize);
  drawGlyph(val, glyphSize);

  if (show_oddball) {
    resetMatrix();
    translate(middle_x - halfSize, middle_y - halfSize);
    highlightGlyph(glyphSize)
  }

  // resetMatrix();
  // translate(middle_x + halfSize + 32, middle_y - halfSize);
  // color_glyph.draw(val, glyphSize);
}

function drawGridMode() {
  let mode = modeSelector.value();

  let glyphSize = parseInt(sizeSelector.value(), 10);
  background(backgroundColor);
  if (show_oddball &&  mode == 'oddball') {
    resetMatrix();
    translate(gridOffsetX + oddball_col * gridSpacingX, gridOffsetY + oddball_row * gridSpacingY);
    highlightGlyph(glyphSize)
  }
  let hexOffset = (mode == "image");
  for (let i=0; i<numGridRows; i++) {
    let tweakedNumGridCols = numGridCols;
    let offsetX = 0;
    if (hexOffset && i%2 == 1) {
      offsetX = gridSpacingX / 2;
      tweakedNumGridCols = numGridCols - 1;
    }
    for (let j=0; j<tweakedNumGridCols; j++) {
      resetMatrix();
      translate(gridOffsetX + j * gridSpacingX + offsetX, gridOffsetY + i * gridSpacingY);
      drawGlyph(gridValues[i][j], glyphSize);
      resetMatrix();
    }
  }
}

function colorCopyArray(c) {
  d = Array(18);
  for(let i=0; i<18; i++) {
    d[i] = Array(18);
    for(let j=0; j<18; j++) {
      d[i][j] = c[i][j];
    }
  }
  return d;
}

function checkImageUpdate() {
  let mode = modeSelector.value();

  isSwappingEmoji = false;
  if (mode == "image") {
    now = millis();
    if(lastKeyPressedTime + 1000 * secondsUntilSwapMode < now) {
      // key not pressed recently
      if(lastEmojiSwappedTime + 1000 * secondsPerEmoji < now) {
        prevEmoji = curEmoji;
        prevEmojiColors = colorCopyArray(curEmojiColors);
        // no swaps recently
        updateEmoji(1);
        nextEmojiColors = colorCopyArray(curEmojiColors);
        lastEmojiSwappedTime = now;
      }
      if(now - lastEmojiSwappedTime < 1000) {
        isSwappingEmoji = true;
        emojiSwapLerp = (now - lastEmojiSwappedTime) / 1000.0;
        // print("LERP: " + emojiSwapLerp);
        for (let i=0; i<numGridRows; i++) {
          for (let j=0; j<numGridCols; j++) {
            // let curColor = lerpColor(prevEmojiColors[i][j], nextEmojiColors[i][j], emojiSwapLerp);
            let curColor = prevEmojiColors[i][j];
            if (curColor) {
              curColor = lerpColor(prevEmojiColors[i][j], nextEmojiColors[i][j], emojiSwapLerp);
              curEmojiPixels[i][j][0] = curColor._getHue();
              curEmojiPixels[i][j][1] = curColor._getSaturation();
              curEmojiPixels[i][j][2] = curColor._getBrightness();
            }
          }
        }
        refreshGridData();
      }
      else {
        for (let i=0; i<numGridRows; i++) {
          for (let j=0; j<numGridCols; j++) {
            let curColor = nextEmojiColors[i][j];
            if (curColor) {
              curEmojiPixels[i][j][0] = curColor._getHue();
              curEmojiPixels[i][j][1] = curColor._getSaturation();
              curEmojiPixels[i][j][2] = curColor._getBrightness();
            }
          }
        }
        refreshGridData();
      }
    }
  }
}

let is_drawing = false;
function draw () {
  if (is_drawing) {
    return;
  }
  is_drawing = true;
  let mode = modeSelector.value();

  if (mode == "sketch") {
    image(sketchImg, 0, 0, width, height);
    is_drawing = false;
    return;
  }

  checkImageUpdate();

  if (mode == 'edit') {
    drawDriveMode();
  }
  else {
    drawGridMode();
  }
  resetMatrix();
  if (mode == "image") {
    image(curEmojiImg, 96, height-32-36);
  }
  is_drawing = false;
}

function keyTyped() {
  if (key == '!') {
    saveBlocksImages();
  }
  else if (key == '@') {
    saveBlocksImages(true);
  }
  else if (key == ' ') {
    refreshGridData();
    redraw();
  }
  else if (key == 'd') {
    let curGlyph = glyphSelector.value()
    if(curGlyph == "color") {
      glyphSelector.value('glyph');
    }
    else if(curGlyph == "glyph") {
      glyphSelector.value('both');
    }
    else if(curGlyph == "both") {
      glyphSelector.value('color');
    }
    redraw();
  }
  else if (key == 'c') {
    let old_value = guideCheckbox.checked();
    guideCheckbox.checked(!old_value);
    guideChangedEvent();
  }
  else if (key == '1') {
    sizeSelector.value('32');
    sizeChangedEvent()
  }
  else if (key == '2') {
    sizeSelector.value('64');
    sizeChangedEvent()
  }
  else if (key == '3') {
    sizeSelector.value('128');
    sizeChangedEvent()
  }
  else if (key == 's') {
    modeSelector.value('sketch');
    modeChangedEvent()
  }
  else if (key == 'e') {
    modeSelector.value('edit');
    modeChangedEvent()
  }
  else if (key == 'g') {
    modeSelector.value('gradient');
    modeChangedEvent()
  }
  else if (key == 'r') {
    modeSelector.value('random');
    modeChangedEvent()
  }
  else if (key == 'o') {
    modeSelector.value('oddball');
    modeChangedEvent()
  }
  else if (key == 'i') {
    modeSelector.value('image');
    modeChangedEvent()
  }
}

function updateEmoji(offset) {
    curEmoji = (curEmoji + NUM_EMOJI + offset) % NUM_EMOJI;
    modeChangedEvent()
}

function keyPressed() {
  lastKeyPressedTime = millis();

  if (keyCode == LEFT_ARROW) {
    updateEmoji(-1);
  }
  else if (keyCode == RIGHT_ARROW) {
    updateEmoji(1);
  }
  else if (keyCode == UP_ARROW) {
    updateEmoji(-38);
  }
  else if (keyCode == DOWN_ARROW) {
    updateEmoji(38);
  }
}

function mouseMoved() {
  lastKeyPressedTime = millis();
}

function mouseDragged() {
  lastKeyPressedTime = millis();
}

z_color_helper.js

function fillHsluv(h, s, l) {
  var rgb = hsluv.hsluvToRgb([h, s, l]);
  fill(rgb[0] * 255, rgb[1] * 255, rgb[2] * 255);
}

function strokeHsluv(h, s, l) {
  var rgb = hsluv.hsluvToRgb([h, s, l]);
  stroke(rgb[0] * 255, rgb[1] * 255, rgb[2] * 255);
}

function fillUniform(brightness) {
    fillHsluv(0, 0, brightness);
}

function strokeUniform(brightness) {
    strokeHsluv(0, 0, brightness);    
}

z_hsluv-0.0.3.min.js

(function() {function f(a){var c=[],b=Math.pow(a+16,3)/1560896;b=b>g?b:a/k;for(var d=0;3>d;){var e=d++,h=l[e][0],w=l[e][1];e=l[e][2];for(var x=0;2>x;){var y=x++,z=(632260*e-126452*w)*b+126452*y;c.push({b:(284517*h-94839*e)*b/z,a:((838422*e+769860*w+731718*h)*a*b-769860*y*a)/z})}}return c}function m(a){a=f(a);for(var c=Infinity,b=0;b<a.length;){var d=a[b];++b;c=Math.min(c,Math.abs(d.a)/Math.sqrt(Math.pow(d.b,2)+1))}return c}
function n(a,c){c=c/360*Math.PI*2;a=f(a);for(var b=Infinity,d=0;d<a.length;){var e=a[d];++d;e=e.a/(Math.sin(c)-e.b*Math.cos(c));0<=e&&(b=Math.min(b,e))}return b}function p(a,c){for(var b=0,d=0,e=a.length;d<e;){var h=d++;b+=a[h]*c[h]}return b}function q(a){return.0031308>=a?12.92*a:1.055*Math.pow(a,.4166666666666667)-.055}function r(a){return.04045<a?Math.pow((a+.055)/1.055,2.4):a/12.92}function t(a){return[q(p(l[0],a)),q(p(l[1],a)),q(p(l[2],a))]}
function u(a){a=[r(a[0]),r(a[1]),r(a[2])];return[p(v[0],a),p(v[1],a),p(v[2],a)]}function A(a){var c=a[0],b=a[1];a=c+15*b+3*a[2];0!=a?(c=4*c/a,a=9*b/a):a=c=NaN;b=b<=g?b/B*k:116*Math.pow(b/B,.3333333333333333)-16;return 0==b?[0,0,0]:[b,13*b*(c-C),13*b*(a-D)]}function E(a){var c=a[0];if(0==c)return[0,0,0];var b=a[1]/(13*c)+C;a=a[2]/(13*c)+D;c=8>=c?B*c/k:B*Math.pow((c+16)/116,3);b=0-9*c*b/((b-4)*a-b*a);return[b,c,(9*c-15*a*c-a*b)/(3*a)]}
function F(a){var c=a[0],b=a[1],d=a[2];a=Math.sqrt(b*b+d*d);1E-8>a?b=0:(b=180*Math.atan2(d,b)/Math.PI,0>b&&(b=360+b));return[c,a,b]}function G(a){var c=a[1],b=a[2]/360*2*Math.PI;return[a[0],Math.cos(b)*c,Math.sin(b)*c]}function H(a){var c=a[0],b=a[1];a=a[2];if(99.9999999<a)return[100,0,c];if(1E-8>a)return[0,0,c];b=n(a,c)/100*b;return[a,b,c]}function I(a){var c=a[0],b=a[1];a=a[2];if(99.9999999<c)return[a,0,100];if(1E-8>c)return[a,0,0];var d=n(c,a);return[a,b/d*100,c]}
function J(a){var c=a[0],b=a[1];a=a[2];if(99.9999999<a)return[100,0,c];if(1E-8>a)return[0,0,c];b=m(a)/100*b;return[a,b,c]}function K(a){var c=a[0],b=a[1];a=a[2];if(99.9999999<c)return[a,0,100];if(1E-8>c)return[a,0,0];var d=m(c);return[a,b/d*100,c]}function L(a){for(var c="#",b=0;3>b;){var d=b++;d=Math.round(255*a[d]);var e=d%16;c+=M.charAt((d-e)/16|0)+M.charAt(e)}return c}
function N(a){a=a.toLowerCase();for(var c=[],b=0;3>b;){var d=b++;c.push((16*M.indexOf(a.charAt(2*d+1))+M.indexOf(a.charAt(2*d+2)))/255)}return c}function O(a){return t(E(G(a)))}function P(a){return F(A(u(a)))}function Q(a){return O(H(a))}function R(a){return I(P(a))}function S(a){return O(J(a))}function T(a){return K(P(a))}
var l=[[3.240969941904521,-1.537383177570093,-.498610760293],[-.96924363628087,1.87596750150772,.041555057407175],[.055630079696993,-.20397695888897,1.056971514242878]],v=[[.41239079926595,.35758433938387,.18048078840183],[.21263900587151,.71516867876775,.072192315360733],[.019330818715591,.11919477979462,.95053215224966]],B=1,C=.19783000664283,D=.46831999493879,k=903.2962962,g=.0088564516,M="0123456789abcdef";
window.hsluv={hsluvToRgb:Q,rgbToHsluv:R,hpluvToRgb:S,rgbToHpluv:T,hsluvToHex:function(a){return L(Q(a))},hexToHsluv:function(a){return R(N(a))},hpluvToHex:function(a){return L(S(a))},hexToHpluv:function(a){return T(N(a))},lchToHpluv:K,hpluvToLch:J,lchToHsluv:I,hsluvToLch:H,lchToLuv:G,luvToLch:F,xyzToLuv:A,luvToXyz:E,xyzToRgb:t,rgbToXyz:u,lchToRgb:O,rgbToLch:P};})();

z_purview_helper.js

// note: this file is poorly named - it can generally be ignored.

// helper functions below for supporting blocks/purview

function saveBlocksImages(doZoom) {
  if(doZoom == null) {
    doZoom = false;
  }

  // generate 960x500 preview.jpg of entire canvas
  // TODO: should this be recycled?
  var offscreenCanvas = document.createElement('canvas');
  offscreenCanvas.width = 960;
  offscreenCanvas.height = 500;
  var context = offscreenCanvas.getContext('2d');
  // background is flat white
  context.fillStyle="#FFFFFF";
  context.fillRect(0, 0, 960, 500);
  context.drawImage(this.canvas, 0, 0, 960, 500);
  // save to browser
  var downloadMime = 'image/octet-stream';
  var imageData = offscreenCanvas.toDataURL('image/jpeg');
  imageData = imageData.replace('image/jpeg', downloadMime);
  p5.prototype.downloadFile(imageData, 'preview.jpg', 'jpg');

  // generate 230x120 thumbnail.png centered on mouse
  offscreenCanvas.width = 230;
  offscreenCanvas.height = 120;

  // background is flat white  
  context = offscreenCanvas.getContext('2d');
  context.fillStyle="#FFFFFF";
  context.fillRect(0, 0, 230, 120);

  if(doZoom) {
    // pixelDensity does the right thing on retina displays
    var pd = this._pixelDensity;
    var sx = pd * mouseX - pd * 230/2;
    var sy = pd * mouseY - pd * 120/2;
    var sw = pd * 230;
    var sh = pd * 120;
    // bounds checking - just displace if necessary
    if (sx < 0) {
      sx = 0;
    }
    if (sx > this.canvas.width - sw) {
      sx = this.canvas.width - sw;
    }
    if (sy < 0) {
      sy = 0;
    }
    if (sy > this.canvas.height - sh) {
      sy = this.canvas.height - sh;
    }
    // save to browser
    context.drawImage(this.canvas, sx, sy, sw, sh, 0, 0, 230, 120);
  }
  else {
    // now scaledown
    var full_width = this.canvas.width;
    var full_height = this.canvas.height;
    context.drawImage(this.canvas, 0, 0, full_width, full_height, 0, 0, 230, 120);
  }
  imageData = offscreenCanvas.toDataURL('image/png');
  imageData = imageData.replace('image/png', downloadMime);
  // call this function after 1 second
  setTimeout(function(){
    p5.prototype.downloadFile(imageData, 'thumbnail.png', 'png');
  }, 1000);
}