|
//Animation ideas: |
|
//- un-draw previous stroke (slowly decrease the arc length of old while increasing new) |
|
//- three-stage move into place, scale, rotate |
|
|
|
var main_canvas; |
|
|
|
var canvasWidth = 960; |
|
var canvasHeight = 500; |
|
|
|
var letters; |
|
var sliders = []; |
|
|
|
var animationProgress = 1.0; |
|
|
|
var colorFront = [227, 222, 212]; |
|
var colorBack = [29, 42, 28]; |
|
|
|
function preload() { |
|
letters = loadJSON('letters.json'); |
|
} |
|
|
|
var attributes = { "X" : new Attribute(0, 1, 0.5), |
|
"Y" : new Attribute(0, 1, 0.5), |
|
"Scale" : new Attribute(0, 1, 0.5), |
|
"Rotation" : new Attribute(0, 2 * Math.PI, 0) |
|
}; //Every (arc) component of a letter has a position, scale, and rotation |
|
|
|
var previousLetter; |
|
var currentLetter = [new CharacterElement(attributes), new CharacterElement(attributes), new CharacterElement(attributes)]; //there are three components per letter |
|
|
|
function Attribute(min, max, defaultValue) { |
|
this.min = min; |
|
this.max = max; |
|
this.defaultValue = defaultValue; |
|
} |
|
|
|
function Point(x, y) { |
|
this.x = x; |
|
this.y = y; |
|
} |
|
|
|
function BoundingBox(min, max) { |
|
this.min = min; |
|
this.max = max; |
|
} |
|
|
|
BoundingBox.forQuarterCircleArc = function(centre, radius, theta) { |
|
// We need to check for the four edges, the point at fromTheta, and the point at toTheta |
|
|
|
var maxX = -Infinity, minX = Infinity, maxY = -Infinity, minY = Infinity; |
|
|
|
// -pi/4 to pi/4 passes through max x |
|
// pi/4 to 3pi/4 passes through max y |
|
// 3pi/4 to 5pi/4 passes through min x |
|
// 5pi/4 to 7pi/4 passes through min y |
|
|
|
if (theta > 1.75 * Math.PI) { |
|
maxX = Math.max(maxX, centre.x + radius); |
|
} else if (theta < 0.75 * PI) { |
|
maxY = Math.max(maxY, centre.y + radius); |
|
} else if (theta < 1.25 * PI) { |
|
minX = Math.min(minX, centre.x - radius); |
|
} else { |
|
minY = Math.min(minY, centre.y - radius); |
|
} |
|
|
|
var startTheta = theta; |
|
var endTheta = theta + Math.PI * 0.5; |
|
|
|
var startX = cos(startTheta) * radius + centre.x; |
|
var startY = sin(startTheta) * radius + centre.y; |
|
var endX = cos(endTheta) * radius + centre.x; |
|
var endY = sin(endTheta) * radius + centre.y; |
|
|
|
minX = Math.min(startX, minX); |
|
minX = Math.min(endX, minX); |
|
|
|
maxX = Math.max(startX, maxX); |
|
maxX = Math.max(endX, maxX); |
|
|
|
minY = Math.min(startY, minY); |
|
minY = Math.min(endY, minY); |
|
|
|
maxY = Math.max(startY, maxY); |
|
maxY = Math.max(endY, maxY); |
|
|
|
return new BoundingBox(new Point(minX, minY), new Point(maxX, maxY)); |
|
} |
|
|
|
BoundingBox.prototype.union = function(boundingBox) { |
|
var box = new BoundingBox(this.min, this.max); |
|
|
|
box.min = new Point(Math.min(this.min.x, boundingBox.min.x), Math.min(this.min.y, boundingBox.min.y)); |
|
box.max = new Point(Math.max(this.max.x, boundingBox.max.x), Math.max(this.max.y, boundingBox.max.y)); |
|
|
|
return box; |
|
} |
|
|
|
/** Performs a deep copy of the passed elements array; since an element array is more or less the same as a letter, this allows us to copy a letter so e.g. we can make modifications to one of the loaded letters while still being able to revert back to the saved version. */ |
|
function copyElements(elements) { |
|
var copied = []; |
|
|
|
for (elementIdx in elements) { |
|
var element = new CharacterElement(elements[elementIdx]); |
|
copied.push(element); |
|
} |
|
|
|
return copied; |
|
} |
|
|
|
|
|
/** A CharacterElement wraps a dictionary of attributes. This constructor takes either a dictionary of Attributes or a plain key-value dictionary. */ |
|
function CharacterElement(attributes) { |
|
for (key in attributes) { |
|
if (attributes[key].defaultValue != undefined) { |
|
this[key] = attributes[key].defaultValue; |
|
} else { |
|
this[key] = attributes[key]; |
|
} |
|
} |
|
} |
|
|
|
CharacterElement.prototype.centered = function(offset) { |
|
var normalised = new CharacterElement(this); |
|
|
|
normalised["X"] = (this["X"] - offset.x); |
|
normalised["Y"] = (this["Y"] - offset.y); |
|
|
|
return normalised; |
|
} |
|
|
|
CharacterElement.prototype.bounds = function() { |
|
var x = this["X"]; |
|
var y = this["Y"]; |
|
var scale = this["Scale"]; |
|
var rotation = this["Rotation"]; |
|
|
|
return BoundingBox.forQuarterCircleArc(new Point(x, y), scale * 0.5, rotation); |
|
} |
|
|
|
/** Computes the bounding box for a letter. */ |
|
function boundsForElements(elements) { |
|
var boundingBox = new BoundingBox(new Point(Infinity, Infinity), new Point(-Infinity, -Infinity)); |
|
|
|
for (elementIdx in elements) { |
|
var element = elements[elementIdx]; |
|
boundingBox = boundingBox.union(element.bounds()); |
|
} |
|
|
|
return boundingBox; |
|
} |
|
|
|
/** Takes a letter, centres it, and returns the centred letter and its scale (since I couldn't figure out how to scale the components without using p5's scale function. */ |
|
function normalisedLetterWithElements(elements) { |
|
|
|
var bounds = boundsForElements(elements); |
|
|
|
// var xRange = bounds.max.x - bounds.min.x; |
|
var yRange = bounds.max.y - bounds.min.y; |
|
var offset = new Point((bounds.min.x + bounds.max.x - 1) * 0.5, (bounds.min.y + bounds.max.y - 1) * 0.5); |
|
|
|
var normalised = []; |
|
|
|
for (elementIdx in elements) { |
|
var element = elements[elementIdx]; |
|
|
|
normalised.push(element.centered(offset)); |
|
} |
|
|
|
return {'letter': normalised, 'scale': 1.0 / yRange }; |
|
} |
|
|
|
/** Generate the controls table containing all of the sliders. */ |
|
function createControlsTable() { |
|
var controlsDiv = document.getElementById("controls"); |
|
var innerText = "<table>"; |
|
|
|
for (element in currentLetter) { |
|
for (attributeName in attributes) { |
|
var attribute = attributes[attributeName]; |
|
|
|
innerText += "<tr><td>" + attributeName + "</td><td id=\"" + attributeName + element + " Container\"></td></tr>"; |
|
} |
|
} |
|
|
|
innerText += "<tr><td>Letter</td><td id=\"selectorContainer\"></td></tr><tr><td></td><td id=\"buttonContainer\"></td></tr></table>"; |
|
controlsDiv.innerHTML = innerText; |
|
|
|
for (elementIdx in currentLetter) { |
|
|
|
var element = currentLetter[elementIdx]; |
|
|
|
for (attributeName in attributes) { |
|
|
|
var attribute = attributes[attributeName]; |
|
|
|
var slider = createSlider(attribute.min, attribute.max, element[attributeName], (attribute.max - attribute.min) * 0.01); |
|
slider.parent(attributeName + elementIdx + " Container"); |
|
|
|
slider.attributeName = attributeName; |
|
slider.elementIdx = elementIdx; |
|
|
|
sliders.push(slider); |
|
} |
|
} |
|
|
|
|
|
var sel = createSelect(); |
|
|
|
for (letter in letters) { |
|
sel.option(letter); |
|
} |
|
sel.changed(letterChangedEvent); |
|
|
|
var button = createButton('show data'); |
|
button.mousePressed(buttonPressedEvent); |
|
|
|
sel.parent("selectorContainer"); |
|
button.parent("buttonContainer"); |
|
|
|
} |
|
|
|
/** Update our model whenever a slider changes its value. */ |
|
function sliderValueChanged(event) { |
|
|
|
var slider = event.target; |
|
var attributeName = slider.attributeName; |
|
var elementIdx = slider.elementIdx; |
|
|
|
currentLetter[elementIdx][attributeName].value = slider.value; |
|
} |
|
|
|
function readValuesFromSliders(sliders) { |
|
for (sliderIdx in sliders) { |
|
var slider = sliders[sliderIdx]; |
|
currentLetter[slider.elementIdx][slider.attributeName] = slider.value(); |
|
} |
|
} |
|
|
|
function setup () { |
|
// create the drawing canvas, save the canvas element |
|
main_canvas = createCanvas(canvasWidth, canvasHeight); |
|
main_canvas.parent('canvasContainer'); |
|
|
|
|
|
for (letter in letters) { |
|
var elements = letters[letter]; |
|
for (elementIdx in elements) { |
|
var element = elements[elementIdx]; |
|
element.__proto__ = CharacterElement.prototype; //Since we've loaded from JSON the prototype isn't correctly set. |
|
} |
|
} |
|
|
|
// createControlsTable(); |
|
|
|
changeToLetter("A"); |
|
} |
|
|
|
function changeToLetter(letter) { |
|
animationProgress = 0.0; |
|
var result = normalisedLetterWithElements(letters[letter]); |
|
previousLetter = currentLetter; |
|
currentLetter = result.letter; |
|
|
|
for (sliderIdx in sliders) { |
|
var slider = sliders[sliderIdx]; |
|
slider.value(currentLetter[slider.elementIdx][slider.attributeName]); |
|
} |
|
} |
|
|
|
function letterChangedEvent(event) { |
|
var item = event.target.value; |
|
changeToLetter(item); |
|
} |
|
|
|
function changeToRandomLetter() { |
|
|
|
var letterKeys = Object.keys(letters); |
|
|
|
var letter; |
|
do { //We don't want to animate back to the same letter. |
|
var index = Math.floor(letterKeys.length * Math.random()); |
|
letter = letterKeys[index]; |
|
} while (letter == currentLetter); |
|
changeToLetter(letter); |
|
} |
|
|
|
function buttonPressedEvent() { |
|
json = JSON.stringify(currentLetter, null, 2); |
|
alert(json); |
|
} |
|
|
|
function saturate(x) { |
|
return max(0, min(1, x)); |
|
} |
|
|
|
/** Interpolates between fromElements and toElements in a custom animation based on percentage. */ |
|
function animateCharacterElements(fromElements, toElements, percentage) { |
|
|
|
var newElements = []; |
|
|
|
var scaledPercentage = percentage * 3.0; |
|
|
|
for (index in fromElements) { |
|
var fromElement = fromElements[index]; |
|
var toElement = toElements[index]; |
|
|
|
var animatedElement = new CharacterElement(fromElement); |
|
|
|
var translationPercentage = saturate(percentage * 2.5); |
|
var scalePercentage = saturate(percentage * 2.1 - 0.6); |
|
var rotationPercentage = saturate(percentage * 3.6 - 2.1); |
|
|
|
animatedElement["X"] = lerp(fromElement["X"], toElement["X"], translationPercentage); |
|
animatedElement["Y"] = lerp(fromElement["Y"], toElement["Y"], translationPercentage); |
|
animatedElement["Scale"] = lerp(fromElement["Scale"], toElement["Scale"], scalePercentage); |
|
animatedElement["Rotation"] = lerp(fromElement["Rotation"], toElement["Rotation"], rotationPercentage); |
|
|
|
newElements.push(animatedElement); |
|
} |
|
return newElements; |
|
} |
|
|
|
/** Draws an individual part of a letter (e.g. an individual arc). */ |
|
function drawFromAttributes(attributes) { |
|
var x = attributes["X"]; |
|
var y = attributes["Y"]; |
|
var rotation = attributes["Rotation"]; |
|
var scale = attributes["Scale"]; |
|
|
|
{ |
|
push(); |
|
|
|
noFill(); |
|
stroke(colorFront); |
|
strokeWeight(0.007); |
|
|
|
var fromAngle = 0 + rotation; |
|
var toAngle = HALF_PI + fromAngle; |
|
|
|
arc(x, y, scale, scale, fromAngle, toAngle); |
|
|
|
pop(); |
|
} |
|
} |
|
|
|
|
|
|
|
function drawLetter(letter) { |
|
for (elementIndex in letter) { |
|
var characterElement = letter[elementIndex]; |
|
drawFromAttributes(characterElement); |
|
} |
|
} |
|
|
|
var timeLastUpdate = 0; |
|
var timeLastKeyPress = 0; |
|
var timeLastAnimationStarted = 0; |
|
|
|
function updateAnimation() { |
|
var currentTime = millis(); |
|
var elapsedTime = currentTime - timeLastUpdate; |
|
|
|
var animationDuration = 800.0; |
|
|
|
if (animationProgress == 0.0) { |
|
timeLastAnimationStarted = currentTime; |
|
} |
|
|
|
animationProgress = Math.min(1.0, animationProgress + elapsedTime / animationDuration); |
|
|
|
timeLastUpdate = currentTime; |
|
|
|
//If we've had 30s of keyboard inactivity and we haven't started an animation in the last 4 seconds |
|
if (currentTime - timeLastKeyPress > 30000 && currentTime - timeLastAnimationStarted > 4000) { |
|
changeToRandomLetter(); |
|
} |
|
} |
|
|
|
function draw () { |
|
|
|
updateAnimation(); |
|
readValuesFromSliders(sliders); |
|
|
|
var minDimension = Math.min(width, height); |
|
|
|
translate((width - minDimension) * 0.5, 0); |
|
scale(minDimension, minDimension); //Work in a square area in the centre of the screen. |
|
|
|
background(colorBack); |
|
fill(colorFront); |
|
stroke(95, 52, 8); |
|
|
|
if (animationProgress < 1.0) { |
|
var letter = animateCharacterElements(previousLetter, currentLetter, animationProgress); |
|
drawLetter(letter); |
|
} else { |
|
drawLetter(currentLetter, 0, animationProgress); |
|
} |
|
} |
|
|
|
function keyTyped() { |
|
timeLastKeyPress = millis(); |
|
if (key == '!') { |
|
saveBlocksImages(); |
|
} |
|
else if (key == '@') { |
|
saveBlocksImages(true); |
|
} else { |
|
var letter = key.toUpperCase(); |
|
if (letters[letter] != null) { |
|
changeToLetter(letter); |
|
} |
|
} |
|
} |