xxxxxxxxxx
550
/**
* Demo of concepts for mapping image data to signal data and vice versa.
* The sig array and the img array contain the "same" values, just in different orders.
* This app shows how they can be mapped to each other using Look Up Tables (LUTs).
* Similar code will be part of PixelAudio, a Processing application + library in the works.
* Description available on request.
*
* By Paul Hertz, Ignotus the Mage, https://paulhertz.net/. Free for non-commercial use.
* If this actually proves useful, I'd appreciate hearing about it.
*/
// slider (created in uiAnimSlider function) is not responding, so it's omitted.
// It works in other, simpler, examples: https://editor.p5js.org/Ignotus_Mago/sketches/CvGN07-Hr
// Including the slider and then calling foucs() and blur() on it will at least allow the
// keyTyped and keyPressed handlers to work. Go figure.
let img = []; // array of color values in bitmap row major order
let sig = []; // array of color values in Hilbert curve order
let hilbDepth = 3; // 3 is a good value, maximum value of 6
let hilbCount; // number of rows or columns in the grid
// for hilbDepth of 2, sigLUT should be [0,1,5,4,8,12,13,9,10,14,15,11,7,6,2,3]
let sigLUT = []; // look up table for index numbers from sig into the img array
let imgLUT = []; // look up table for index numbers from img into the sig array
const sigClip = {p : 0, l : 0, arr : []}; // values plucked from sig
const imgClip = {w : 0, h : 0, arr : []}; // values peeled from img
let w = 1088; // width of canvas for display of sig data and img data side-by-side
let h = 512; // height of canvas
let squareW = 512; // width of the squares where we draw patterns
let isLoadSignal = true;
let isShowPath = false;
let isPauseAnim = true;
let mapImgToSigBtn;
let mapSigToImgBtn;
let canv;
let frameStep = 15;
function setup() {
canv = createCanvas(w, h);
canv.id('signalToImageCanvas');
frameRate(30);
let hilb = new HilbGen(hilbDepth);
hilbCount = hilb.mapWidth; // number of rows or columns in the grid
if (hilbCount > w/16) console.log("hilbDepth should not exceed 6.");
imgLUT = hilb.indexMap;
for (let i = 0; i < imgLUT.length; i++) {
sigLUT[imgLUT[i]] = i;
}
if (hilbDepth < 5) console.log("sigLUT: ", sigLUT);
if (hilbDepth < 5) console.log("imgLUT: ", imgLUT);
if (hilbDepth < 5) console.log("hilb.iMap: ", hilb.indexMap);
gridW = h/hilbCount; // width of grid elements
gridH = h/hilbCount; // height of grid elements
gridHalf = gridW/2; // half the width of an element
initSignal(); // store color values in sig
initUI(); // set up User Interface
pluck(0,16);
}
function draw() {
if (frameCount % frameStep == 1) {
if (!isPauseAnim) {
rotateDataRight();
updateDisplay();
}
}
fillSquares();
if (isShowPath) drawSignalPath();
}
/** initialize the colors in sig and img */
function initSignal() {
let steps = sigLUT.length;
let ang = 45;
let inc = 360/steps;
colorMode(HSL, 360, 100, 100);
if (isLoadSignal) {
for (let i = 0; i < sigLUT.length; i++) {
let c = color((ang + round(i * inc)) % 360, 60, 50); // generate color spectrum
sig[i] = c; // assign current color to sig
img[sigLUT[i]] = c; // map the same color to img
}
}
else {
for (let i = 0; i < sigLUT.length; i++) {
let c = color((ang + round(i * inc)) % 360, 60, 50); // generate color spectrum
img[i] = c; // assign current color to img
sig[imgLUT[i]] = c; // map the same color to sig
// console.log("image index", imgLUT[i], hue(c));
}
}
fillSquares();
}
/** user interface, mostly buttons */
function initUI() {
let canvElem = document.getElementById('signalToImageCanvas'); // DOM ref canvas
let canvParent = canvElem.parent; // DOM ref to canvas parent
let xPos = 12; // track horizontal position
let yPos = 524; // track vertical position
// labels for signal and image (sig and img) displays
let sigLabel = createDiv('signal');
sigLabel.position(squareW/2, yPos);
let imgLabel = createDiv('image');
imgLabel.position(width - squareW/2, yPos);
yPos += 24;
// txt1
let txt1 = createDiv('Click on the canvas to flip a color.');
txt1.position(xPos, yPos);
txt1.parent(canvParent);
// uiAnimSlider(xPos + 460, yPos); /* uncomment to show (non-functional) slider */
yPos += 24;
// txt2
let txt2 = createDiv('Operations: ');
txt2.position(xPos, yPos);
xPos += 84;
txt2.parent(canvParent);
// mapSigToImgBtn
mapSigToImgBtn = createButton("mapSigToImg");
mapSigToImgBtn.id('mapSig');
mapSigToImgBtn.position(xPos, yPos);
mapSigToImgBtn.mousePressed(mapSigToImg);
mapSigToImgBtn.parent(canvParent);
xPos += mapSigToImgBtn.width + 16;
// mapImgToSigBtn
mapImgToSigBtn = createButton("mapImgToSig");
mapImgToSigBtn.id('mapImg');
mapImgToSigBtn.position(xPos, yPos);
mapImgToSigBtn.mousePressed(mapImgToSig);
mapImgToSigBtn.parent(canvParent);
xPos += mapImgToSigBtn.width + 16;
// writeSigToImgBtn
writeSigToImgBtn = createButton("writeSigToImg");
writeSigToImgBtn.id('writeSig');
writeSigToImgBtn.position(xPos, yPos);
writeSigToImgBtn.mousePressed(writeSigToImg);
writeSigToImgBtn.parent(canvParent);
xPos += writeSigToImgBtn.width + 16;
// writeImgToSigBtn
writeImgToSigBtn = createButton("writeImgToSig");
writeImgToSigBtn.id('writeImg');
writeImgToSigBtn.position(xPos, yPos);
writeImgToSigBtn.mousePressed(writeImgToSig);
writeImgToSigBtn.parent(canvParent);
xPos += writeImgToSigBtn.width + 16;
// resetBtn
let resetBtn = createButton("Reset");
resetBtn.id('reset');
resetBtn.mousePressed(() => {
isLoadSignal = true; initSignal();});
resetBtn.position(xPos, yPos);
resetBtn.parent(canvParent);
xPos += resetBtn.width + 16;
// pathCheckbox
let pathCheckbox = createCheckbox(" Draw Path", isShowPath);
pathCheckbox.id('path');
pathCheckbox.position(xPos, yPos);
pathCheckbox.mouseReleased(() => {isShowPath = !isShowPath;});
pathCheckbox.parent(canvParent);
xPos += 120;
// pauseCheckbox
let pauseCheckbox = createCheckbox(" Pause", isPauseAnim);
pauseCheckbox.id('pause');
pauseCheckbox.position(xPos, yPos);
pauseCheckbox.mouseReleased(() => {isPauseAnim = !isPauseAnim;});
yPos += 28;
xPos = 12;
// stepLBtn
let stepLBtn = createButton("Step Left");
stepLBtn.id('stepleft');
stepLBtn.position(xPos, yPos);
stepLBtn.mouseReleased(() => {rotateDataLeft(); updateDisplay();});
xPos += stepLBtn.width + 16;
// stepRBtn
let stepRBtn = createButton("Step Right");
stepRBtn.id('stepright');
stepRBtn.position(xPos, yPos);
stepRBtn.mouseReleased(() => {rotateDataRight(); updateDisplay();});
xPos += stepRBtn.width + 16;
// pluckBtn
let pluckBtn = createButton("Pluck");
pluckBtn.id('pluck');
pluckBtn.position(xPos, yPos);
pluckBtn.mousePressed(() => {pluck(0, hilbCount * 2);
console.log("--- sigClip", sigClip);});
xPos += pluckBtn.width + 16;
// plantBtn
let plantBtn = createButton("Plant");
plantBtn.id('plant');
plantBtn.position(xPos, yPos);
plantBtn.mousePressed(() => {plant(round(hilbCount * hilbCount / 2)); mapSigToImg();});
xPos += plantBtn.width + 16;
// peelBtn
// omit for now
// stampBtn
let stampBtn = createButton("Stamp");
stampBtn.id('stamp');
stampBtn.position(xPos, yPos);
stampBtn.mousePressed(() => {stamp(hilbCount/4, hilbCount/4, hilbCount/2, hilbCount/2);
mapImgToSig(); });
}
// UI Element that is not called && because not really functional.
// Leaving it here while I think about why the slider is not responsive.
function uiAnimSlider(xPos, yPos) {
/**/
let canvElem = document.getElementById('signalToImageCanvas'); // DOM ref to canvas
let canvParent = canvElem.parent; // DOM ref to canvas parent
// txt3
let txt3 = createDiv("FPS");
txt3.id('fps');
xPos += 108;
txt3.position(xPos, yPos);
txt3.parent(canvParent);
// animSlider
// SLIDER is still not working, but focus and blur calls enable keyTyped() - hacky!
let animSlider = createSlider(1, 120, 10);
animSlider.id('anim');
animSlider.size(120);
xPos += 40;
animSlider.position(xPos, yPos - 4);
animSlider.parent(canvParent);
let anim = document.getElementById("anim");
anim.addEventListener('change',function() {this.setAttribute('value',this.value);});
// maybe we can control the HTML to see why the range input doesn't work...?
// anim.outerHTML = '<input type="range" min="1" max="120" id="anim" value="40" style="width: 120px; position: absolute; left: 331px; top: 572px;" width="120" height="0">';
// Calling focus() and blur() enables key stroke capture with keyTyped()
// It's a hacky workaround and the slider still doesn't work.
// One could also click the "Draw Path" checkbox twice to enable capture.
anim.focus();
anim.blur();
/**/
}
/** rotate data array right */
function rotateDataRight() {
if (isLoadSignal) {
sig.unshift(sig.pop()); // rotate sig array right
}
else {
img.unshift(img.pop()); // rotate img array right
}
}
/** rotate data array left */
function rotateDataLeft() {
if (isLoadSignal) {
sig.push(sig.shift()); // rotate sig array left
}
else {
img.push(img.shift()); // rotate img array left
}
}
/** update the display by mapping changes from sig or img to img or sig */
function updateDisplay() {
if (isLoadSignal) {
mapSigToImg(); // copy sig values to img array at mapped positions
}
else {
mapImgToSig(); // copy img values to sig array at mapped positions
}
}
/** draw the Hilbert curve and row-major paths */
function drawSignalPath() {
let offset = w - 512;
push();
stroke(255);
noFill();
strokeWeight(2);
beginShape();
for (let i = 0; i < sigLUT.length; i++) {
let p = sigLUT[i];
let x = p % hilbCount * gridW + gridHalf;
let y = floor(p/hilbCount) * gridH + gridHalf;
vertex(x,y);
}
endShape();
beginShape();
for (let i = 0; i < imgLUT.length; i++) {
let p = i;
let x = p % hilbCount * gridW + gridHalf;
let y = floor(p/hilbCount) * gridH + gridHalf;
vertex(offset + x,y);
}
endShape();
pop();
}
/** paint the canvas with colored squares using color values from img or sig */
function fillSquares() {
let offset = w - 512;
push();
for (let i = 0; i < sigLUT.length; i++) {
// get the index number stored at sigLUT[i] to draw in Hilbert Curve order
let p = sigLUT[i];
// convert the index to coordinates
let x = p % hilbCount * gridW;
let y = floor(p/hilbCount) * gridH;
// get the color value from sig[i]
fill(sig[i]);
noStroke();
square(x, y, gridW);
}
for (let i = 0; i < imgLUT.length; i++) {
// we draw the image in row major order, the order of values in img[]
let p = i;
// convert the index to coordinates
let x = p % hilbCount * gridW;
let y = floor(p/hilbCount) * gridH;
// get the color value from img[i], which maps its colors from sigLUT
fill(img[i]);
noStroke();
square(offset + x, y, gridW);
}
pop();
}
/** catch and handle mousePressed events */
function mousePressed() {
if (pointInCanvas(mouseX, mouseY)) {
// get x and y coordinates, scale them to the grid
let x = floor(mouseX/gridW);
let y = floor(mouseY/gridH);
// console.log(mouseX, mouseY, x, y);
let p = x + y * hilbCount; // convert the coordinates into index value p for imgLUT
let i = imgLUT[p]; // retrieve the index value i for sigLUT from imgLUT
// get the color from sig[i]
let c = sig[i];
// console.log("----->>> p = "+ p +", i = "+ i +", c = "+ c);
colorMode(HSL, 360, 100, 100);
let h = Math.floor(hue(c));
// console.log("--->> new hue:", h);
c = color((h + 180) % 360, 90, 80);
sig[i] = c;
img[sigLUT[i]] = c;
fillSquares();
}
return false;
}
// non-functional handler
// click twice on Draw Path checkbox to enable focus for key stroke capture?
// or maybe not even that will work. More p5js quirks with the DOM.
function keyTyped() {
console.log("--->> key "+ key);
return false;
}
function keyPressed() {
if (keyCode === LEFT_ARROW) {
frameStep = frameStep > 2 ? frameStep - 1 : frameStep;
} else if (keyCode === RIGHT_ARROW) {
frameStep = frameStep < 120 ? frameStep + 1 : frameStep;
}
print("--- frameStep =", frameStep);
}
/** did user click in the canvas? */
function pointInCanvas(x, y) {
return (x >= 0 && x <= 512 && y >= 0 && y <= height);
}
/** map changes from the img array to the sig array */
function mapImgToSig() {
/*
// same results, different order
for (let i = 0; i < img.length; i++) {
sig[imgLUT[i]] = img[i];
}
*/
for (let i = 0; i < sig.length; i++) {
sig[i] = img[sigLUT[i]];
}
}
/** map changes from the sig array to the img array */
function mapSigToImg() {
/*
// same story, different narrative
for (let i = 0; i < sig.length; i++) {
img[sigLUT[i]] = sig[i];
}
*/
for (let i = 0; i < img.length; i++) {
img[i] = sig[imgLUT[i]];
}
}
/** skip the mapping, write img directly to sig */
function writeImgToSig() {
for (let i = 0; i < img.length; i++) {
sig[i] = img[i];
}
}
/** skip the mapping, write sig directly to img */
function writeSigToImg() {
for (let i = 0; i < sig.length; i++) {
img[i] = sig[i];
}
}
/** grab a subsequence from sig, stash it in sigClip */
function pluck(pos, len) {
if (pos + len > sig.length) {
sigClip.p = pos;
sigClip.l = sig.length - pos;
sigClip.arr = sig.slice(pos, sig.length);
}
else {
sigClip.p = pos;
sigClip.l = len;
sigClip.arr = sig.slice(pos, pos + len);
}
}
/** overwrite a sequence in sig with the elements from sigClip */
function plant(pos) {
console.log("--- plant at index", pos)
let j = 0;
if (pos + sigClip.arr.length > sig.length) {
for (let i = pos; i < sig.length; i++) {
sig[i] = sigClip.arr[j++];
}
}
else {
for (let i = pos; i < pos + sigClip.arr.length; i++) {
sig[i] = sigClip.arr[j++];
}
}
}
/** not implemented, for later development: grab a rectangular block of pixels from img */
function peel(x, y, w, h) {
}
/** overwrite a block pixels in img -- should be followed by mapImgToSig() */
function stamp(xloc, yloc, wid, hgt) {
//console.log("--- stamp xloc, yloc: ", xloc, yloc);
//console.log("--- stamp wid, hgt: ", wid, hgt);
/**/
let j = 0;
let k = 0;
colorMode(RGB, 255);
for (j = yloc; j < yloc + hgt; j++) {
for (k = xloc; k < xloc + wid; k++) {
// console.log("--- stamp j, k", j, k);
let pos = j * hilbCount + k;
//console.log("--- stamp pos", pos);
let c = color(21, 34, 55);
img[pos] = c;
}
}
/**/
}
// LUT generator class creates the lookup table for Hilbert curve
class HilbGen {
/** set instance variables here */
constructor(depth) {
this.depth = depth;
this.d = round(pow(2, depth));
this.mapW = this.d;
this.mapH = this.d;
this.n = this.d * this.d;
console.log("--->> HilbGen: ", "depth", this.depth, " width", this.d, " size", this.n);
this.bertX = 0;
this.bertY = 0;
this.iMap = [this.n];
this.doXYSwap = (depth % 2 == 1);
this.generateMap();
}
// an analytical method for generating Hilbert curve coordinates
// doesn't use recursion
generateMap() {
let index = 0;
for (let i = 0; i < this.n; i++) {
this.d2xy(this.n, i);
index = this.bertX + this.d * this.bertY;
// console.log(i, "index "+ index +", bertX "+ this.bertX +", bertY "+ this.bertY);
this.iMap[index] = i;
}
}
// methods to return iMap, mapW, mapH, probably all the interface strictly needs
// unlike Java, we can't reuse property names for getter names (?)
get mapWidth() {
return this.mapW;
}
get mapHeight() {
return this.mapH;
}
get indexMap() {
return this.iMap;
}
// called by generateMap(), change distance along a curve to x,y coordinates
d2xy(pts, pos) {
let rx = 0;
let ry = 0;
let t = pos;
let s = 0;
this.bertX = 0;
this.bertY = 0;
for (let s = 1; s < pts; s *= 2) {
rx = 1 & (floor(t/2));
ry = 1 & (floor(t ^ rx));
this.rot(s, rx, ry);
this.bertX += s * rx;
this.bertY += s * ry;
t = floor(t/4);
}
if (this.doXYSwap) {
this.swapXY();
}
}
// called by d2xy(), swap this.bertX and this.bertY
swapXY() {
let temp = this.bertY;
this.bertY = this.bertX
this.bertX = temp;
}
// called by d2xy(), symmetry transform
rot(s, rx, ry) {
if (ry == 0) {
if (rx == 1) {
this.bertX = s - 1 - this.bertX;
this.bertY = s - 1 - this.bertY;
}
// swap bertX and bertY
this.swapXY()
}
}
} // end class HilbGen