Last active
March 10, 2017 05:18
-
-
Save hunterloftis/a31c88ef7dd8e9263c124fd60ffef06b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<head> | |
<title>JS Paint</title> | |
<style> | |
* { margin: 0; padding: 0; box-sizing: border-box; font-family: arial, sans-serif; } | |
body { background: #eee; } | |
#wrap { position: relative; margin: 50px auto 0 auto; width: 800px; } | |
#canvas { width: 800px; height: 600px; background: #fff; box-shadow: 1px 1px 32px #ddd; cursor: crosshair; } | |
#tools { position: absolute; width: 150px; top: 0; left: -150px; font-size: 18px; } | |
form { padding-top: 50px; } | |
label { display: block; margin-bottom: 10px; } | |
</style> | |
</head> | |
<body> | |
<div id='wrap'> | |
<canvas id='canvas' width='800' height='600'></canvas> | |
<div id='tools'> | |
<form name='tools'> | |
<label><input type='radio' name='tool' value='pen' checked='checked'> pen</label> | |
<label><input type='radio' name='tool' value='drip'> drip</label> | |
<label><input type='radio' name='tool' value='spraypaint'> spraypaint</label> | |
<label><input type='radio' name='tool' value='sketch'> sketch</label> | |
<label><input type='radio' name='tool' value='fill'> fill</label> | |
<label><input type='radio' name='tool' value='marker'> marker</label> | |
</form> | |
<form name='colors'> | |
<label><input type='radio' name='color' value='#00272B' checked='checked'> black</label> | |
<label><input type='radio' name='color' value='#E6AF2E'> yellow</label> | |
<label><input type='radio' name='color' value='#6096BA'> blue</label> | |
<label><input type='radio' name='color' value='#037171'> green</label> | |
<label><input type='radio' name='color' value='#EF5B5B'> red</label> | |
</form> | |
</div> | |
</div> | |
<script> | |
// Themes: ES6 (ES2017), testability, decoupling, indirection | |
// Function hoisting ("Iceberg" style) | |
const canvas = document.getElementById('canvas') | |
const context = Painting(canvas) | |
Pen(canvas, context) | |
Drip(canvas, context, 25) | |
Spraypaint(canvas, context, 25, 50) | |
Sketch(canvas, context) | |
Fill(canvas, context) | |
Marker(canvas, context, 20) | |
function Painting(canvas) { | |
const ctx = canvas.getContext('2d') | |
const tool = document.forms.tools.elements.tool | |
const color = document.forms.colors.elements.color | |
const brush = { down: false, x: 0, y: 0 } | |
// Observer pattern | |
canvas.addEventListener('mousedown', start) | |
canvas.addEventListener('mousemove', move) | |
canvas.addEventListener('mouseup', stop) | |
canvas.addEventListener('mouseleave', stop) | |
setInterval(update) | |
// Crockford style ("Better parts") | |
return ctx | |
function start(e) { | |
let detail = { x: e.offsetX, y: e.offsetY, color: color.value } | |
canvas.dispatchEvent(new CustomEvent(`${tool.value}:start`, { detail })) | |
Object.assign(brush, { down: true, x: e.offsetX, y: e.offsetY }) | |
} | |
function move(e) { | |
if (!brush.down) return | |
let detail = { x: [brush.x, e.offsetX], y: [brush.y, e.offsetY], color: color.value } | |
canvas.dispatchEvent(new CustomEvent(`${tool.value}:move`, { detail })) | |
Object.assign(brush, { x: e.offsetX, y: e.offsetY }) | |
} | |
function stop(e) { | |
let detail = { x: brush.x, y: brush.y, color: color.value } | |
canvas.dispatchEvent(new CustomEvent(`${tool.value}:stop`, { detail })) | |
Object.assign(brush, { down: false }) | |
} | |
function update() { | |
if (!brush.down) return | |
let detail = { x: brush.x, y: brush.y, color: color.value } | |
canvas.dispatchEvent(new CustomEvent(`${tool.value}:update`, { detail })) | |
} | |
} | |
// dependency injection | |
function Pen(source, ctx) { | |
source.addEventListener('pen:start', (e) => { | |
ctx.globalAlpha = 1 | |
ctx.lineWidth = 1 | |
ctx.strokeStyle = e.detail.color || 'black' | |
}) | |
source.addEventListener('pen:move', (e) => { | |
ctx.beginPath() | |
ctx.moveTo(e.detail.x[0], e.detail.y[0]) | |
ctx.lineTo(e.detail.x[1], e.detail.y[1]) | |
ctx.stroke() | |
}) | |
} | |
function Marker(source, ctx, size) { | |
source.addEventListener('marker:start', (e) => { | |
ctx.lineWidth = size | |
ctx.globalAlpha = 0.3 | |
ctx.strokeStyle = e.detail.color || 'black' | |
ctx.lineCap = 'square' | |
ctx.lineJoin = 'round' | |
}) | |
source.addEventListener('marker:move', (e) => { | |
ctx.beginPath() | |
ctx.moveTo(e.detail.x[0], e.detail.y[0]) | |
ctx.lineTo(e.detail.x[1], e.detail.y[1]) | |
ctx.stroke() | |
}) | |
} | |
function Spraypaint(source, ctx, dots, spread) { | |
source.addEventListener('spraypaint:start', (e) => { | |
ctx.globalAlpha = 1 | |
ctx.fillStyle = e.detail.color || 'black' | |
}) | |
source.addEventListener('spraypaint:update', (e) => { | |
for (var i = 0; i < dots; i++) { | |
let randX = e.detail.x + (Math.random() - 0.5) * spread | |
let randY = e.detail.y + (Math.random() - 0.5) * spread | |
ctx.fillRect(randX, randY, 1, 1) | |
} | |
}) | |
} | |
function Drip(source, ctx, size) { | |
source.addEventListener('drip:update', (e) => { | |
let randX = e.detail.x + (Math.random() - 0.5) * size | |
let randY = e.detail.y + (Math.random() - 0.5) * size | |
ctx.drawImage(source, randX, randY, 5, 20, randX, randY + 1, 5, 20) | |
}) | |
} | |
function Sketch(source, ctx) { | |
source.addEventListener('sketch:start', (e) => { | |
ctx.lineWidth = 1 | |
ctx.globalAlpha = 0.1 | |
ctx.strokeStyle = e.detail.color || 'black' | |
}) | |
source.addEventListener('sketch:move', (e) => { | |
const dx = e.detail.x[1] - e.detail.x[0] | |
const dy = e.detail.y[1] - e.detail.y[0] | |
ctx.beginPath() | |
ctx.moveTo(e.detail.x[0] - dx, e.detail.y[0] - dy) | |
ctx.lineTo(e.detail.x[1], e.detail.y[1]) | |
ctx.stroke() | |
}) | |
} | |
// breadth-first traversal | |
function Fill(source, ctx) { | |
source.addEventListener('fill:start', (e) => { | |
let imageData = ctx.getImageData(0, 0, source.width, source.height) | |
let checked = new Array(source.width * source.height) | |
let match = getPixel(e.detail.x, e.detail.y) | |
let queue = [[e.detail.x, e.detail.y]] | |
let fillColor = strToRGBA(e.detail.color || '#000000') | |
while (queue.length) { | |
let [x, y] = queue.pop() | |
let directions = [[x, y - 1], [x, y + 1], [x - 1, y], [x + 1, y]] | |
setPixel(x, y, fillColor) | |
directions.forEach(check) | |
} | |
ctx.putImageData(imageData, 0, 0) | |
function getPixel(x, y) { | |
const index = (x + y * source.width) * 4 | |
return imageData.data.slice(index, index + 4) | |
} | |
function setPixel(x, y, color) { | |
const index = (x + y * source.width) * 4 | |
imageData.data.set(color, index) | |
} | |
function check([x, y]) { | |
if (x < 0 || y < 0 || x >= source.width || y >= source.height) return | |
if (checked[x + y * source.width]) return | |
checked[x + y * source.width] = true | |
if (getPixel(x, y).every((rgb, i) => match[i] === rgb)) { | |
queue.push([x, y]) | |
} | |
} | |
}) | |
// binary math | |
function strToRGBA(str) { | |
const hex = `0x${str.slice(1)}` | |
return [hex >> 16, hex >> 8 & 0xff, hex & 0xff, 255] | |
} | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment