Paint

Paint.html  [download]

<!DOCTYPE html>

<html>
<head>

<title>Comp 86 example</title>
<!-- Based on: Marijn Haverbeke, Eloquent JavaScript, http://eloquentjavascript.net/index.html -->

<script type="text/javascript">

/*
 * Global list of tools, indexed by (user-visible) tool name
 */
tools = {}; //*1 List of tools, indexed by (user-visible) tool name

/*
 * Individual controls/widgets
 */

class ToolControl {
    constructor (cx) {
	var select = elt("select"); //*1
	for (var toolname in tools) { //*1
	    select.appendChild (elt ("option", null, toolname)); //*1
	}

	cx.canvas.addEventListener ("mousedown", function(event) { //*2 Set mousedown callback for canvas
	    if (event.which == 1) { //*2
		tools[select.value].down (event, cx); //*1
		event.preventDefault(); //*3 Prevent standard javascript event action
	    }
	});

    	this.elt = elt ("span", null, "Tool: ", select);
    }
}

class ColorControl {
    constructor (cx) {
	// Some browsers will provide a color picker, others default to text field
	var input = elt("input", {type: "color"});

	input.addEventListener("change", function() {
	    cx.fillStyle = input.value;
	    cx.strokeStyle = input.value;
	});

	this.elt = elt ("span", null, "Color: ", input);
    }
}

class BrushControl {
    constructor (cx) {
	var select = elt("select");
	var sizes = [1, 2, 3, 5, 8, 12, 25, 35, 50, 75, 100];
	sizes.forEach (function(size) {
	    select.appendChild (elt("option", {value: size},
                           size + " pixels"));
	});

	select.addEventListener("change", function() {
	    cx.lineWidth = select.value;
	});

	this.elt = elt("span", null, "Brush size: ", select);
    }
}

/*
 * Drawing modes (tools)
 */

/* Base class contains common routines, used by several subclasses */
class Tool {
    trackDrag(onMove, onEnd) { //*4 Base class: common code for all drawing modes
	addEventListener("mousemove", onMove); //*4
	addEventListener("mouseup", end); //*4

	function end(event) { //*4
	    removeEventListener("mousemove", onMove); //*4
	    removeEventListener("mouseup", end); //*4
	    if (onEnd) onEnd(event); //*4
	}
    }

    /*
     * Utility function: Convert from window-relative to element-relative position
     * event.clientX and element.getBoundingClientRect() are both relative to top-left of screen
     */
    static relativePos(event, element) {
	var rect = element.getBoundingClientRect();
	return {x: Math.floor(event.clientX - rect.left),
	    y: Math.floor(event.clientY - rect.top)};
    }
}

class LineTool extends Tool { //*5 Specific code for line drawing mode subclass
    down (event, cx, onEnd) { //*5
	cx.lineCap = "round";

	var pos = Tool.relativePos (event, cx.canvas);
	this.trackDrag(function(event) { //*5
	    cx.beginPath(); //*5
	    cx.moveTo(pos.x, pos.y); //*5
	    pos = Tool.relativePos(event, cx.canvas); //*5
	    cx.lineTo(pos.x, pos.y); //*5
	    cx.stroke(); //*5
	}, onEnd); //*5
    }
}

class EraseTool extends Tool { //*6 Just like drawing except change rasterop then change it back
    down (event, cx) { //*6
	cx.globalCompositeOperation = "destination-out"; //*6
	tools.Line.down (event, cx, function() { //*6
	    cx.globalCompositeOperation = "source-over"; //*6
	});
    }
}

class TextTool extends Tool {
    down (event, cx) {
	var text = prompt("Text:", "");
	if (text) {
	    var pos = Tool.relativePos(event, cx.canvas);
	    cx.font = Math.max(7, cx.lineWidth) + "px sans-serif";
	    cx.fillText(text, pos.x, pos.y);
	}
    }
}

class SprayTool extends Tool {
    down (event, cx) {
	var radius = cx.lineWidth / 2;
	var area = radius * radius * Math.PI;
	var dotsPerTick = Math.ceil(area / 30);

	var currentPos = Tool.relativePos(event, cx.canvas);
	var spray = setInterval(function() {
	    for (var i = 0; i < dotsPerTick; i++) {
		var offset = SprayTool.randomPointInRadius(radius);
		cx.fillRect(currentPos.x + offset.x,
			    currentPos.y + offset.y, 1, 1);
	    }
	}, 25);

	this.trackDrag (function(event) {
	    currentPos = Tool.relativePos(event, cx.canvas);
	}, function() {
	    clearInterval(spray);
	});
    }

    static randomPointInRadius (radius) {
	while (true) { //*7 Random point inside circle
	    var x = Math.random() * 2 - 1; //*7
	    var y = Math.random() * 2 - 1; //*7
	    if (x * x + y * y <= 1) //*7
	        return {x: x * radius, y: y * radius}; //*7
	}
    }
}

/*
 * Main object
 */
class Main {
    constructor () {
	var canvas = elt("canvas", {width: 500, height: 300});
	var cx = canvas.getContext("2d");
	
	tools.Line = new LineTool (); //*1
	tools.Erase = new EraseTool (); //*1
	tools.Text = new TextTool (); //*1
	tools.Spray = new SprayTool (); //*1

	var toolbar = elt("div", {class: "toolbar"});
	toolbar.appendChild (new ToolControl (cx).elt);
	toolbar.appendChild (new ColorControl (cx).elt);
	toolbar.appendChild (new BrushControl (cx).elt);

	var panel = elt("div", {class: "picturepanel"}, canvas);
	document.body.appendChild (elt ("div", null, panel, toolbar));
    }
}

/*
 * Utility function: creates a new DOM element with specified name and
 * attributes and appends all further arguments it gets as child nodes,
 * automatically converting strings to text nodes.
 */
function elt(name, attributes) {
  var node = document.createElement(name);

  if (attributes) {
    for (var attr in attributes) {
      if (attributes.hasOwnProperty(attr)) {
        node.setAttribute(attr, attributes[attr]);
      }
    }
  }

  for (var i = 2; i < arguments.length; i++) {
    var child = arguments[i];
    if (typeof child == "string") {
      child = document.createTextNode(child);
    }
    node.appendChild(child);
  }

return node;
}

window.onload = function () {
    new Main ();
}
</script>

<style>
.picturepanel {
    width: 500px;
    height: 300px;
    border: 3px solid #336699;
    overflow: auto;
    position: relative;
}

.picturepanel canvas {
    display: block;
}

.toolbar > * {
    margin-right: 5px;
}

.toolbar {
    font-family: arial, sans-serif;
    color: #336699;
}
</style>

</head>
</html>