Skip to content

Instantly share code, notes, and snippets.

@hunterloftis
Last active March 10, 2017 05:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hunterloftis/a31c88ef7dd8e9263c124fd60ffef06b to your computer and use it in GitHub Desktop.
Save hunterloftis/a31c88ef7dd8e9263c124fd60ffef06b to your computer and use it in GitHub Desktop.
<!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