xxxxxxxxxx
// Drawing - Record to JSON
// @clayheaton
// Record your drawing in a JSON format that p5.js can parse
// Companion sketch for how to display the JSON is here:
// https://openprocessing.org/sketch/1450971
// Smoothing and thinning reference:
// https://jackschaedler.github.io/handwriting-recognition/
// Model:
// A "design" is made of many strokes.
// Drawings are recorded when mouseIsPressed.
// Releasing the mouse ends a "stroke." A design can contain multiple strokes.
// When you are done with a design, press the space bar to save that design,
// and to start drawing a new one.
// The blue lines that appear are a representation of the mathematically smoothed and thinned
// versions of the lines that you drew. These are the lines that will be saved and exported.
// Press 's' to increase smoothing and 'a' to decrease smoothing (matching your original drawing more closely).
// Thinning down-samples your original drawing, creating fewer data points and smaller export files.
// Press 't' to increase thinning and 'r' to decrease thinning (matching your original drawing more closely).
// The red dot represents the centroid of a design. When exporting a file, each design will draw in reference
// to it's specific red dot. By default, these are centered. You may need to draw in reference to a different point.
// For example, if you draw a leaf, you might want the reference point at the end of the stem. That would allow you
// to place the leaf on a point on the stem and the leaf would appear to grow out of the stem.
// To change the reference point for a design, press 'p' and then click once where you want the reference point to be.
// If you press 'p' again, the reference will return to the centroid. Pressing yet again will allow you to place it again.
// The centroid will appear grey when a custom reference point is placed. The custom reference point icon appears as
// a set of concentric circles.
// OUT OF DATE:
// When you are completely done, press the space bar to save the design
// and then press 'e' to export all of the designs in a JSON file.
// See here for how to parse the JSON and play it back:
// https://openprocessing.org/sketch/1450971
// Smoothing and thinning is on a per-design basis. Before pressing the space bar
// to save a design and clear the canvas, adjust the smoothing and thinning to an
// appropriate level.
// Default values for adjustments to thinning and smoothing
let smoothingFactor = 0; //0.5;
let thinningFactor = 0; //0.4;
let thinningDist = 20;
let factorAdjust = 0.01;
let exportTime = true;
// Exports are multiplied by this
// 1 keeps all data in the -1, 1 range
// 100 puts data in the -100, 100 range, etc.
let exportScale = 100;
let exportSigFigs = 0;
let exportCompress = true;
let drawing = false;
let smoothedStrokeBuffer = [];
let smoothedAndThinnedStrokeBuffer = [];
let strokeBuffer = [];
let smoothedDesignBuffer = [];
let smoothedAndThinnedDesignBuffer = [];
let designBuffer = [];
let completedDesigns = [];
let thinSmoothDesignPoints = 0;
let designPoints = 0;
let reducedPct = "0%";
let showState = 0;
const SHOW_BOTH = 0;
const SHOW_ORIG = 1;
const SHOW_SMOOTHED = 2;
let showInstructions = true;
let useCustomAnchor = false;
let armAnchorClick = false;
let anchorPoint, designCentroid;
let anchorPointBuffer = [];
let designCentroidBuffer = [];
const TEMPLATE_CIRCLE = 0;
const TEMPLATE_SQUARE = 1;
const TEMPLATE_FONT = 2;
const TEMPLATE_SQUARE_RATIO = 0.3;
const TEMPLATE_FONT_X_RATIO = 0.2; // fix this
let minDim;
let img;
const TEMPLATES = [TEMPLATE_CIRCLE, TEMPLATE_SQUARE, TEMPLATE_FONT];
let visibleTemplate = 1;
let fixedSquareMode = false; // Bounds are set at the square template
let fixedCenterMode = false; // Center point remains at template center
let fontMode = false; // for use when creating fonts
let c;
function setup() {
c = createCanvas(windowWidth, windowHeight);
c.drop(processDrop);
minDim = min(width, height);
background(255);
noFill();
strokeWeight(2);
textSize(14);
anchorPoint = createVector(width / 2, height / 2);
designCentroid = createVector(width / 2, height / 2);
}
let lapse = 0;
function processDrop(file) {
img = createImg(file.data, '').hide();
}
function draw() {
background(255);
if (img) {
image(img, width * 0.5 - img.width / 2, height * 0.5 - img.height / 2, img.width, img.height);
}
drawTemplate();
// Steal a click if placing the anchor point.
if (armAnchorClick) {
// mouseReleased catches an anchor point.
} else {
// Don't draw if setting anchor point
if (mouseIsPressed) {
drawing = true;
let o = {
"x": mouseX,
"y": mouseY,
"t": round(millis())
};
strokeBuffer.push(o);
} else {
if (drawing) {
drawing = false;
processStrokeBuffer();
}
}
}
// Current stroke
if (showState == SHOW_BOTH || showState == SHOW_SMOOTHED) {
if (smoothedStrokeBuffer.length > 0) {
stroke(0, 0, 255, 100);
beginShape();
smoothedStrokeBuffer.forEach(p => {
vertex(p.x, p.y);
});
endShape();
}
}
if (showState == SHOW_BOTH || showState == SHOW_ORIG) {
if (strokeBuffer.length > 0) {
stroke(0);
beginShape();
strokeBuffer.forEach(p => {
vertex(p.x, p.y);
});
endShape();
}
}
// Prior strokes
if (showState == SHOW_BOTH || showState == SHOW_SMOOTHED) {
if (smoothedDesignBuffer.length > 0) {
smoothedDesignBuffer.forEach(sb => {
stroke(0, 0, 255, 100);
beginShape();
sb.forEach(p => {
ellipse(p.x, p.y, 4, 4);
vertex(p.x, p.y);
});
endShape();
})
}
}
if (showState == SHOW_BOTH || showState == SHOW_ORIG) {
if (designBuffer.length > 0) {
designBuffer.forEach(sb => {
stroke(0);
beginShape();
sb.forEach(p => {
vertex(p.x, p.y);
});
endShape();
})
}
}
smoothedStrokeBuffer = smoothPoints(strokeBuffer);
smoothedDesignBuffer = smoothStrokes(designBuffer);
showAnchorPoint();
if (showInstructions) {
fill(0);
noStroke();
textAlign(LEFT);
text("Press 'h' to toggle original (black) and smoothed/thinned (blue) line visibility.", width - 300, height - 150, 310, 80);
if (fixedSquareMode) {
text("FIXED SQUARE MODE ACTIVE", width - 300, height - 300);
} else if (fixedCenterMode) {
text("FIXED CENTER MODE ACTIVE", width - 300, height - 300);
}
text("Press and hold mouse to draw.", width - 300, height - 270);
text("Press Space to store design.", width - 300, height - 240);
text("Press 'e' to export JSON data.", width - 300, height - 210);
text("Press 'u' to undo last stroke.", width - 300, height - 180);
text("Smoothing Factor: " + smoothingFactor + " a(-), s(+)", width - 300, height - 80);
text("Thinning Factor: " + thinningFactor + " r(-), t(+)", width - 300, height - 50);
text("Data reduced by " + reducedPct, width - 300, height - 20);
noFill();
}
}
function mouseReleased() {
if (millis() - lapse > 200) {
if (armAnchorClick) {
anchorPoint = createVector(mouseX, mouseY);
armAnchorClick = false;
}
}
lapse = millis();
}
function showAnchorPoint() {
let minX = width;
let maxX = 0;
let minY = height;
let maxY = 0;
let pointCount = 0;
for (let strokeNum = 0; strokeNum < smoothedDesignBuffer.length; strokeNum++) {
let strk = smoothedDesignBuffer[strokeNum];
for (let pointNum = 0; pointNum < strk.length; pointNum++) {
let pt = strk[pointNum];
minX = min(minX, pt.x);
maxX = max(maxX, pt.x);
minY = min(minY, pt.y);
maxY = max(maxY, pt.y);
pointCount++
}
}
if (pointCount > 0) {
fill(255, 0, 0);
noStroke();
let x = minX + (maxX - minX) * 0.5;
let y = minY + (maxY - minY) * 0.5;
designCentroid = createVector(x, y);
if (useCustomAnchor) {
fill(255, 0, 0);
noStroke();
ellipse(anchorPoint.x, anchorPoint.y, 7, 7);
noFill();
stroke(0, 255, 0);
ellipse(anchorPoint.x, anchorPoint.y, 14, 14);
stroke(0, 0, 255);
ellipse(anchorPoint.x, anchorPoint.y, 21, 21);
noStroke();
fill(0, 100);
ellipse(designCentroid.x, designCentroid.y, 6, 6);
} else {
anchorPoint = createVector(x, y);
if (fixedCenterMode) {
designCentroid = createVector(width / 2, height / 2);
anchorPoint = createVector(width / 2, height / 2);
fill(0, 255, 0);
}
ellipse(anchorPoint.x, anchorPoint.y, 6, 6);
noFill();
}
}
}
function keyPressed() {
if (key == " ") {
processDesignBuffer();
}
// Smoothing
if (key == "s") {
smoothingFactor += factorAdjust;
smoothingFactor = round(constrain(smoothingFactor, 0, 1), 2);
}
if (key == "a") {
smoothingFactor -= factorAdjust;
smoothingFactor = round(constrain(smoothingFactor, 0, 1), 2);
}
// Thinning
if (key == "t") {
thinningFactor += factorAdjust;
thinningFactor = round(constrain(thinningFactor, 0, 1), 2);
}
if (key == "r") {
thinningFactor -= factorAdjust;
thinningFactor = round(constrain(thinningFactor, 0, 1), 2);
}
// Change what displays
if (key == "h") {
showState += 1;
if (showState > 2) showState = 0;
}
// Undo
if (key == "u") {
strokeBuffer.pop();
smoothedStrokeBuffer.pop();
designBuffer.pop();
smoothedDesignBuffer.pop();
}
// Use custom anchor
if (key == "p") {
useCustomAnchor = !useCustomAnchor;
if (useCustomAnchor) {
armAnchorClick = true;
fixedCenterMode = false;
fixedSquareMode = false;
fontMode = false;
} else {
armAnchorClick = false;
}
}
// Template display
if (key == "c") {
visibleTemplate += 1;
if (visibleTemplate >= TEMPLATES.length) {
visibleTemplate = -1;
}
if (visibleTemplate == TEMPLATE_FONT){
showInstructions = false;
} else {
showInstructions = true;
}
}
// Fixed Square Mode
// This creates a dummy stroke that is the size
// of the square template, for setting proper scaling.
if (key == "m") {
fixedSquareMode = !fixedSquareMode;
if (fixedSquareMode) {
fixedCenterMode = false;
fontMode = false;
useCustomAnchor = false;
}
}
// Fixed Center Mode
// The center point remains at the template center
if (key == "k") {
fixedCenterMode = !fixedCenterMode;
if (fixedCenterMode) {
fixedSquareMode = false;
useCustomAnchor = false;
fontMode = false;
}
}
// Export
if (key == "e") {
performExport();
}
}
function processStrokeBuffer() {
designBuffer.push(strokeBuffer);
strokeBuffer = [];
smoothedStrokeBuffer = [];
}
function performExport() {
let toExport;
if (exportCompress) {
toExport = [];
completedDesigns.forEach(dsgn => {
let new_dsgn = [];
dsgn.forEach(strk => {
let new_strk = [];
strk.forEach(pnt => {
new_strk.push(pnt.x);
new_strk.push(pnt.y);
if (exportTime) new_strk.push(pnt.t);
});
new_dsgn.push(new_strk);
});
toExport.push(new_dsgn);
});
} else {
toExport = completedDesigns;
}
let exportDesigns = {
scale: exportScale,
compress: exportCompress,
hasTime: exportTime,
payload: toExport
};
saveJSON(exportDesigns, "designs_" + frameCount + ".json", true);
}