A simple animated watercolor paintbrush. Works best in Chrome.
A Pen by Riley Shaw on CodePen.
<p id="tip">Click and drag to draw. You can draw anywhere.</p> | |
<div id="toggles"> | |
<p id="cycling">T: Cycling off</p> | |
<p id="watercolor">W: Watercolor off</p> | |
<p>S: Save image</p> | |
</div> |
function createCanvasElement (width, height, ratio, id, insertAfter) { | |
// Creates a scaled-up canvas based on the device's | |
// resolution, then displays it properly using styles | |
function createHDCanvas (ratio) { | |
var canvas = document.createElement('canvas'); | |
var context = canvas.getContext('2d'); | |
// Creates a dummy canvas to test device's pixel ratio | |
ratio = ratio || (function () { | |
var context = document.createElement('canvas').getContext('2d'); | |
var dpr = window.devicePixelRatio || 1; | |
var bsr = context.webkitBackingStorePixelRatio || | |
context.mozBackingStorePixelRatio || | |
context.msBackingStorePixelRatio || | |
context.oBackingStorePixelRatio || | |
context.backingStorePixelRatio || 1; | |
return dpr / bsr; | |
})(); | |
canvas.width = width * ratio; | |
canvas.height = height * ratio; | |
canvas.style.width = width + 'px'; | |
canvas.style.height = height + 'px'; | |
context.setTransform(ratio, 0, 0, ratio, 0, 0); | |
if (id) canvas.id = id; | |
return canvas; | |
} | |
var canvas = createHDCanvas(ratio); | |
canvas.relativeMouseCoords = function (event) { | |
var totalOffsetX = 0, totalOffsetY = 0, canvasX = 0, canvasY = 0, currentElement = this; | |
do { | |
totalOffsetX += currentElement.offsetLeft - currentElement.scrollLeft; | |
totalOffsetY += currentElement.offsetTop - currentElement.scrollTop; | |
currentElement = currentElement.offsetParent; | |
} while (currentElement); | |
return { | |
x: event.pageX - totalOffsetX, | |
y: event.pageY - totalOffsetY | |
}; | |
}; | |
canvas.download = function () { | |
var imgLink = document.createElement('a'); | |
var img = new Image(); | |
img.src = canvas.toDataURL('image/png'); | |
context.fillStyle = '#fffbf8'; | |
context.fillRect(0, 0, width, height); | |
context.drawImage(img, 0, 0); | |
img = canvas.toDataURL('image/png'); | |
imgLink.href = img; | |
imgLink.download = 'watercolors'; | |
imgLink.click(); | |
}; | |
if (insertAfter) insertAfter.parentNode.insertBefore(canvas, insertAfter.nextSibling); | |
else body.appendChild(canvas); | |
return canvas; | |
}; | |
function hslToRgb (h, s, l) { | |
function hueToRgb (p, q, t) { | |
if (t < 0) t += 1; | |
if (t > 1) t -= 1; | |
if (t < 1/6) return p + (q - p) * 6 * t; | |
if (t < 1/2) return q; | |
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; | |
return p; | |
} | |
var r, g, b, q, p; | |
if (s === 0) { | |
r = g = b = l; // achromatic | |
} else { | |
q = l < 0.5 ? l * (1 + s) : l + s - l * s; | |
p = 2 * l - q; | |
r = hueToRgb(p, q, h + 1/3); | |
g = hueToRgb(p, q, h); | |
b = hueToRgb(p, q, h - 1/3); | |
} | |
return [ | |
Math.round(r * 255), | |
Math.round(g * 255), | |
Math.round(b * 255) | |
]; | |
} | |
function Spot (x, y, radius, hueOffset) { | |
this.x = x; | |
this.y = y; | |
this.radius = radius; | |
this.hueOffset = hueOffset; | |
} | |
function drawSpot (spot) { | |
var hue = (hueBase + spot.hueOffset) % 360 / 360; | |
var gradient = context.createLinearGradient(spot.x - spot.radius, spot.y - spot.radius, spot.x + spot.radius, spot.y + spot.radius); | |
gradient.addColorStop(0, 'rgba(' + hslToRgb(hue, 0.75, 1) + ', ' + opacity + ')'); | |
gradient.addColorStop(1, 'rgba(' + hslToRgb(hue, 0.4, 0.85) + ', ' + opacity + ')'); | |
context.beginPath(); | |
context.arc(spot.x, spot.y, spot.radius, 0, PI2); | |
context.fillStyle = gradient; | |
context.fill(); | |
context.closePath(); | |
} | |
function draw (x, y) { | |
var spot; | |
hueOffsetBase = (hueOffsetBase + hueDelta) % 360; | |
radius = radius + radiusDelta; | |
if (radius <= 0) { | |
init(); | |
return true; | |
} else { | |
spot = new Spot(x, y, radius, hueOffsetBase); | |
drawSpot(spot); | |
spots.push(spot); | |
} | |
} | |
function drawBetween (x1, y1, x2, y2) { | |
var xDiff, yDiff, maxDiff, xDelta, yDelta, x, y; | |
xDiff = x1 - x2; | |
yDiff = y1 - y2; | |
maxDiff = Math.max(Math.abs(xDiff), Math.abs(yDiff)); | |
xDelta = xDiff / maxDiff; | |
yDelta = yDiff / maxDiff; | |
for (var i = 0; i < maxDiff; i++) { | |
x = x2 + i * xDelta; | |
y = y2 + i * yDelta; | |
if (draw(x, y)) return; | |
} | |
lastCoords = {x: x1, y: y1}; | |
} | |
function init () { | |
drawMode = false; | |
radius = 60; | |
lastCoords = {}; | |
setTimeout(function () { body.className = ''; }, 1000); | |
} | |
function tick () { | |
hueBase = (360 + hueBase - 3) % 360; | |
context.clearRect(0, 0, width, height); | |
spots.forEach(drawSpot); | |
if (cycling) requestAnimationFrame(tick); | |
} | |
var PI2 = Math.PI * 2; | |
var body = document.body; | |
var cyclingToggle = document.getElementById('cycling'); | |
var watercolorToggle = document.getElementById('watercolor'); | |
var width = document.documentElement.clientWidth; | |
var height = window.innerHeight; | |
var canvas = createCanvasElement(width, height, 1, 'worm', document.getElementById('toggles')); | |
var context = canvas.getContext('2d'); | |
var x1 = 300; | |
var y1 = 300; | |
var hueBase = 240; | |
var hueOffsetBase = 0; | |
var hueDelta = 0.4; | |
var drawMode, radius, lastCoords; | |
var radiusDelta = -0.02; | |
var spots = []; | |
var cycling = true; | |
var opacity = 0.01; | |
init(); | |
canvas.addEventListener('mousedown', function (event) { | |
var coords = canvas.relativeMouseCoords(event); | |
draw(coords.x, coords.y); | |
lastCoords = coords; | |
drawMode = true; | |
body.className = 'fadeOut'; | |
}, false); | |
canvas.addEventListener('mouseup', init, false); | |
canvas.addEventListener('mousemove', function (event) { | |
if (drawMode) { | |
var coords = canvas.relativeMouseCoords(event); | |
drawBetween(coords.x, coords.y, lastCoords.x, lastCoords.y); | |
lastCoords = coords; | |
} | |
}, false); | |
document.addEventListener('keydown', function (event) { | |
var keyCode = event.keyCode; | |
if (keyCode === 83) canvas.download(); | |
else if (keyCode === 84) { | |
if (cycling) cyclingToggle.textContent = 'T: Cycling on'; | |
else { | |
cyclingToggle.textContent = 'T: Cycling off'; | |
requestAnimationFrame(tick); | |
} | |
cycling = !cycling; | |
} else if (keyCode === 87) { | |
if (opacity === 0.01) { | |
watercolor.textContent = 'W: Watercolor on'; | |
opacity = 1; | |
} else { | |
watercolor.textContent = 'W: Watercolor off'; | |
opacity = 0.01; | |
} | |
} | |
}, false); | |
requestAnimationFrame(tick); |
body { | |
overflow: hidden; | |
margin: 0px; | |
font: bold 24px/1.5em sans-serif; | |
color: #c8c4b9; | |
background: #fffbf8; | |
cursor: crosshair; | |
} | |
#tip, #toggles { | |
position: fixed; | |
left: 1em; | |
transition: opacity 2s 20s; | |
pointer-events: none; | |
} | |
#tip { | |
top: 1em; | |
} | |
#toggles { | |
bottom: 1em; | |
} | |
p { | |
width: 10em; | |
margin: 0; | |
} | |
.fadeOut #tip, .fadeOut #toggles { | |
opacity: 0; | |
transition: opacity 2s; | |
} |
A simple animated watercolor paintbrush. Works best in Chrome.
A Pen by Riley Shaw on CodePen.