draw.html at [4ceaad09ba]
Not logged in

File draw.html artifact 00f152a683 part of check-in 4ceaad09ba


<html>
<script src='matrix_vector_lib.js' ></script>
<script src='wireframe_model.js'></script>

<script>
/*
//  TODO rotate selected points
//  TODO control with zoom rotate or pan means that action is only applied to the camera not the selected points.
//  TODO clicking and dragging on a line allows you to place a point somewhere on that line.  Perhaps with the modifier key it would break up the existing line into line segments.
//  TODO when lines are deleted, readd the solo points to the solo_points list
//  TODO z for undo shift + z for redo.
//  TODO 
//  TODO drop down minimalist menu in top right of canvas.



//
//
//  Global variables
//
//  use longer more descriptive names, especially for obscure variables, to avoid conflict in the global space,
//  even if certain properties are factored into objects, if it is a large object, they work similar to globals in that exist in a large namespace accessible from many functions.
//
//  All caps indicates a constant
//  Some lower case variables may also be constants
//  Capitalized constants indicate that we expect that variable to remain a constant
//  even in future code changes, ie statically configured with an initial.
//
//



var canvas;

var frame_rate = 20;
var saveTimeout = 10*1000;



var last_zoom_dist = 1;
var zoom_factor = 0.83;



// camera animation
var max_move_delta = 0.01;
var max_angle_delta = Math.PI;

var delta_horizontal_angle = 0;
var delta_vertical_angle = 0;
var delta_position = [0,0,0];




var selected_points = [];
var selected_lines = [];

var helpmsg = //"3D drawing tool\n\n" +
              "Select     Left Click\n" +
              "Rotate     Right Click\n" +
              "Move       wasd, qe\n" +
              "Copy       Space\n" +
              "Delete     x\n" +
              "\n" +
              "Click the red dot to create a point\n" +
              "Spam a motion key to increase speed\n" +
              "Drag between points to connect them\n";
	          
			  
var msg = helpmsg;


var view_transform =
            [[1, 0, 0],
             [0, 1, 0],
             [0, 0, 1]];


var key_state = {};
var key_state_augmentation_timeout = 150; // the permissible time in milliseconds since the last key event to still allow the key state to augment instead of clearing.




// constants
var MIN_DRAG_DIST = 10;
var POINT_HIGHLIGHT_DIST = 10;
var LINE_HIGHLIGHT_DIST = 15;

var MOTION_SPEED_BASE =  2.8;

var CENTER_POINT_COLOR = "rgba(250, 0, 0, 1.0)";
var AXIS_POINT_COLOR = "rgba(0, 0, 250, 1.0)";

var CENTER_POINT_RADIUS = 2.5;
var MIN_AXIS_POINT_RADIUS = 1;
var MAX_AXIS_POINT_RADIUS = 4;
var AXIS_LINE_WIDTH = 1.333;

var FORWARD_AXIS_LEN = 0.15;
var VERTICAL_AXIS_LEN = 0.075;


//  The point, line, and fill colors are the same for a reason.
//  Because all the colors are the same we don't have to worry about occlusion and z-order,
//  because no matter which order colors are painted onto a pixel, the final result will be the same.
//  Observe that this assertion holds even with a color with an alpha channel.
//  This helps us use our 2d canvas happily and not have to worry about draw order or non-sensical visual effects from wrong order.
//  With transparency we can still see each individual object and it almost mimics occlusion.
//  This is meant to be a lightweight drawing tool, not a full featured one, so this decision is limiting but fits our goals perfectly.

var POINT_COLOR = "rgba(0, 0, 0, 0.9)";
var LINE_COLOR = "rgba(0, 0, 0, 0.9)";
var FILL_COLOR = "rgba(0, 0, 0, 0.9)";

var LINE_WIDTH = 1;


function getKeyState(code){
	if(!key_state[code]) return 0;
	return key_state[code].state; }



function addKeyListener(keycode, func){
    if(!key_state[keycode]) key_state[keycode] = {};
    
    var key_obj = key_state[keycode];
    if(!key_obj.listeners) key_obj.listeners = [];
    
    var listeners = key_obj.listeners;
    listeners.push(func);
}


function keyDown(event, code){
    var time = (new Date()).getTime();
    var code = event.keyCode;
    // alert("you pressed: " + code);
        
    var key_obj = key_state[code];
    if(!key_obj) key_obj = key_state[code] = {}; // create object

    
    //  update state etc.
    
    var state = key_obj.state;
    if(state) return; //key is already pressed.
    
    state = 0;
    
    var latest = key_obj.latest;

    if(latest && time - latest < key_state_augmentation_timeout)
        state = key_obj.state = key_obj.lastState + 1;
    else
        state = key_obj.state = 1;
        
    key_obj.latest = time;
    
    key_obj.lastState = state;


    //  call listener functions 
    var listeners = key_obj.listeners
    if(!listeners) return;  //nothing to do.
      
    for(var i = 0; i < listeners.length; ++i)
        listeners[i](event, state, state); }

    
function keyUp(event){
    var time = (new Date()).getTime();
    var code = event.keyCode;
    
    var key_obj = key_state[code];
    if(!key_obj) return;  //nothing to do.
      
    key_obj.latest = time;
    key_obj.lastState = key_obj.state;
    var state = key_obj.state = 0;
    var lastState = key_obj.lastState;
    
    var listeners = key_obj.listeners;
    if(!listeners) return;  //nothing to do.
    
    for(var i = 0; i < listeners.length; ++i)
        listeners[i](event, state, lastState); }

        
        
        
function setUiEvents(){
    document.body.onkeydown = keyDown;
    document.body.onkeyup = keyUp;
    
    document.body.onmousedown = function(event){
        var x = event.pageX;
        var y = event.pageY;
        x -= canvas.offsetLeft;
        y -= canvas.offsetTop;
        last_mouse_down = [x, y];
        
        var button = event.button;
        var keycode = button + 1000;  //treat clicks like a button press but with higher keycode.
        keyDown({keyCode: keycode}); }  // it seems the keyCode of the original event cant be overwritten.
        
    document.body.onmousewheel = function(event){
        var delta = event.wheelDelta;
        
		if(delta < 0)
			zoom_dist /= zoom_factor;
		else if(delta > 0)
			zoom_dist *= zoom_factor; }
        
        
    document.body.onmouseup = function(event){
        var button = event.button;
        var keycode = button + 1000;  //treat clicks like a button press but with higher keycode.
        keyUp({keyCode: keycode});
        last_mouse_down = null;
        mouse_dragging = false; }}
        


    
function changeVelocity(direction, positive, state, lastState){
    if(state == 0){
        state = lastState;
        positive = !positive; }  // reverse the velocity
    
    var delta = Math.pow(MOTION_SPEED_BASE, state - 1) * max_move_delta / frame_rate;
    if(!positive) delta = -delta;
    
    var velocityChange = [0, 0, 0];
    velocityChange[direction] = delta;
    vector_add(delta_position, velocityChange, delta_position); }
    

	

    
function leftPress(e, s, lasts){
    changeVelocity(0, false, s, lasts); }
    
function rightPress(e, s, lasts){
     changeVelocity(0, true, s, lasts); }
     
function downPress(e, s, lasts){
    changeVelocity(1, false, s, lasts); }
    
function upPress(e, s, lasts){
    changeVelocity(1, true, s, lasts); }

function backwardPress(e, s, lasts){
    changeVelocity(2, false, s, lasts); }
    
function forwardPress(e, s, lasts){
    changeVelocity(2, true, s, lasts); }
        
        
function zoomInPress(e, s, lasts){
    zoom_dist *= zoom_factor; }
        
        
function zoomOutPress(e, s, lasts){
    zoom_dist *= zoom_factor; }


function copySelectedPoints(e, s, lasts){
    if(s){
        var new_selection = [];
        var selection_map = {};
        
        for(var i = 0; i < selected_points.length; ++i){
            selection_map[selected_points[i]] = i;
            var pt = points[selected_points[i]];
            
            var newpt = addPoint(pt);
            new_selection[i] = newpt; }
        
        //if(getKeyState(16)){  //shift + space copies lines as well.
        
        // copy lines as well
        for(var i = 0; i < lines.length; ++i){
           var line = lines[i];
           if(!line) continue;
           
           var pt1 = lines[i][0];
           var pt2 = lines[i][1];
           var i1 = selection_map[pt1];
           var i2 = selection_map[pt2];
           
           if(i1 !== undefined && i2 !== undefined){  // both points are selected
               addLine(new_selection[i1], new_selection[i2]); }}
       
        selected_points = new_selection; }}
            
            
addKeyListener(32, copySelectedPoints);        
    
addKeyListener(37, leftPress);  // arrow keys
addKeyListener(38, forwardPress);
addKeyListener(39, rightPress);
addKeyListener(40, backwardPress);


addKeyListener(65, leftPress);  // wasd
addKeyListener(87, forwardPress);
addKeyListener(68, rightPress);
addKeyListener(83, backwardPress);
addKeyListener(69, upPress);
addKeyListener(81, downPress);


addKeyListener(88, function(e, s, lasts){  // x

    if(s){
        if(highlight_object !== null && highlight_object > 0){  // delete highlighted object
            
            if(highlight_object < point_projections.length){  // point
                points[highlight_object] = null; }
                
            else if(highlight_object < point_projections.length + line_midpoint_projections.length){  // line
                lines[highlight_object - point_projections.length] = null; }
            else throw "highlight object index to large"; }
        
        else if(selected_points.length){  // delete selected points
            for(var i = 0; i < selected_points.length; ++i){
                points[selected_points[i]] = null; }
            selected_points.length = 0; }
        
        cleanupDeletedPoints();
        highlight_object = null;
        selected_points.length = 0; }});

		
//  select point
addKeyListener(1000, function(e, s, lasts){

	if(s){
		if(highlight_object !== null){
			if(highlight_object == -1)
				highlight_object = addPoint(origin);  // create a new point at the origin
				
			
			var alreadySelected = false;  // only set for shift clicks
			
			if(getKeyState(16)){  // shift key is pressed.
			
				for(var i = 0; i < selected_points.length; ++i){
					if(selected_points[i] == highlight_object){
					   selected_points.splice(i, 1); // deselect point
					   alreadySelected = true; 
					   break; }}}
			else{
				selected_points.length = 0;}
				
			if(!alreadySelected && highlight_object < points.length) selected_points.push(highlight_object); }

		else if(!getKeyState(16))
			selected_points.length = 0; }
		
		
	else if(lasts && mouse_dragging ){  // drag actions
		
		//  connect selected points to highlight point with lines
		if(selected_points.length && (!getKeyState(16))){  //  TODO right now this really only works for drawing a single line, 
		
			if(highlight_object !== null && highlight_object < point_projections.length){
				if(highlight_object == -1) highlight_object = addPoint(origin);
				
				for(var i = 0; i < selected_points.length; ++i){
					var pt = selected_points[i];
					if(pt == highlight_object) continue;
					addLine(pt, highlight_object); }
				selected_points.length = 0; }}
			
		// select points inside selection rectangle
		else if(mouse_loc && last_mouse_down){ 
		
			var minx = Math.min(mouse_loc[0], last_mouse_down[0]);
			var maxx = Math.max(mouse_loc[0], last_mouse_down[0]);
			var miny = Math.min(mouse_loc[1], last_mouse_down[1]);
			var maxy = Math.max(mouse_loc[1], last_mouse_down[1]);
			
			
			var selected_point_map = {};             
			
			if(!getKeyState(16)){
				selected_points.length = 0;
				selected_lines.length = 0; }
			else{
				for(var i = 0; i < selected_points.length; ++i){
					selected_point_map[selected_points[i]] = i; }}
			
			for(var i = 0; i < point_projections.length; ++i){
				if(selected_point_map[i]) continue; // already selected.
				
				var pt = point_projections[i];
				if(!pt) continue;
				var _x = pt[0];
				var _y = pt[1];
				
				if(minx < _x && _x < maxx
				 && miny < _y && _y < maxy){
				   selected_points.push(i); }}}                
        }});


// right click
addKeyListener(1002, function(e, s, lasts){
	if(s == 0){
		delta_horizontal_angle = 0;
		delta_vertical_angle = 0; }
		
	//else if(getKeyState(16)){  // use default as rotation center
		//rotation_center = origin.slice(0); }
		
	else if(highlight_object > 0){  // use highlighted points for rotation center.
		if(highlight_object < points.length){
			rotation_center = points[highlight_object].slice(0); }
			
		else{
			var line = lines[highlight_object];
			var pointa = points[line[0]];
			var pointb = points[line[1]];
			rotation_center = vector_midpoint(pointa, pointb);
			selection_rotation_axis = vector_minus(pointa, pointb);
			if(vector_dot(selection_rotation_axis, view_transform[2]) < 0){
				vector_scale(-1, selection_rotation_axis, selection_rotation_axis); }}}  // axis should be pointing vertical, horizontal motions along the screen will dictate motion angle.
	
	else if(selected_points.length){  //find average of selected points for rotation center
		rotation_center = [0,0,0];
	    for(var i = 0; i < selected_points.length; ++i){
			vector_add(rotation_center, points[selected_points[i]], rotation_center); }
		var l = selected_points.length;
		rotation_center = [rotation_center[0]/l, rotation_center[1]/l, rotation_center[2]/l];
        // alert("Rotation center: " + rotation_center);

		}});


function writeMsg(canvas, msg){
	var lines = msg.split("\n");
	var ctx = canvas.getContext("2d");
	ctx.font = "9pt sans-serif"
	for(var i = 0; i < lines.length; ++i){
		ctx.fillText(lines[i], 5, 10 + 16 *  i); }}

    
//  camera functions

function rotateView(norm, theta, cameracentric){

    // camera centric uses the camera axes to perform the rotation instead of the space axes.   
    if(cameracentric) norm = matrix_mult(view_transform, [norm])[0];
    var rotation = vector_rotation(norm, theta);
    
    view_transform = matrix_mult(rotation, view_transform); }
	
	
    
    
function rotateHorizontal(theta){
    rotateView([0, 1, 0], theta, false); }
    
function rotateVertical(theta){
    if(vertical_camera_rotation + theta > Math.PI/2)
        theta = Math.PI/2 - vertical_camera_rotation;
        
    if(vertical_camera_rotation + theta < -Math.PI/2)
        theta = -Math.PI/2 - vertical_camera_rotation;
        
    vertical_camera_rotation += theta;
    
    rotateView([1, 0, 0], theta, true); }
    

    
    
window.onload = function(){

    // load localStorage saved state.
    if(localStorage.points && localStorage.lines){
        points = JSON.parse(localStorage.points);
        lines = JSON.parse(localStorage.lines); }
    
    canvas = document.createElement('canvas');
    document.body.appendChild(canvas);
    
    canvas.width = window.innerWidth - 20;
    canvas.height = window.innerHeight - 20;
    
    
    window.onresize = function(){
        canvas.width = window.innerWidth - 20;
        canvas.height = window.innerHeight - 20; }


    function getRotationAngle(x, size, max, center_size){

        if(!center_size) center_size = 0.05;  // the proportional size of the area in which no rotation is effected by the mouse movement
        if(!max) max = Math.PI/128;
        
        //  center does nothing
        if(Math.abs(x) < size * center_size/2) x = 0;
        else if(x < 0) x += size * center_size/2;
        else           x -= size * center_size/2;
        
        return max * x / ((1 - center_size) * size/2); }
        
    
    document.body.onmousemove = function(event){
    //var test = function(event){

        var x = event.pageX;
        var y = event.pageY;
        x -= canvas.offsetLeft;
        y -= canvas.offsetTop;
        mouse_loc = [x, y]

        var w = canvas.width;
        var h = canvas.height;
        
        if(!mouse_dragging && last_mouse_down){
            var _x = x - last_mouse_down[0];
            var _y = y - last_mouse_down[1];
            
            var dist2 = _x*_x + _y*_y;
            
            if(dist2 > MIN_DRAG_DIST * MIN_DRAG_DIST){
                mouse_dragging = true; }}
                

				
	    if(getKeyState(1002) && last_mouse_down){
			delta_horizontal_angle = getRotationAngle(x - last_mouse_down[0], w, max_angle_delta/frame_rate, 0);
			delta_vertical_angle = -getRotationAngle(y - last_mouse_down[1], h, max_angle_delta/frame_rate, 0); }
			
			
		var min_point_dist2 = LINE_HIGHLIGHT_DIST * LINE_HIGHLIGHT_DIST;
		highlight_object = null;
		
		var _x = canvas.width/2 - x;
		var _y = canvas.height/2 - y;
		var point_dist = _x*_x + _y*_y;
		
		if(point_dist < POINT_HIGHLIGHT_DIST * POINT_HIGHLIGHT_DIST){
			highlight_object = -1;
			min_point_dist = point_dist; }
			
			
		for(var i = 0; i < line_midpoint_projections.length; ++i){
			var pt = line_midpoint_projections[i];
			if(!pt) continue;
			
			var _x = pt[0] - x;
			var _y = pt[1] - y;
			point_dist = _x*_x + _y*_y;
			
			if(point_dist < min_point_dist2){  // alert('highlighting line');
				highlight_object = i + point_projections.length;
				min_point_dist = point_dist; }}
				
		min_point_dist2 = Math.min( min_point_dist2, POINT_HIGHLIGHT_DIST * POINT_HIGHLIGHT_DIST);
			
		for(var i = 0; i < point_projections.length; ++i){
			var pt = point_projections[i];
			if(!pt) continue;
			
			var _x = pt[0] - x;
			var _y = pt[1] - y;
			point_dist = _x*_x + _y*_y;
			
			if(point_dist < min_point_dist2){
				highlight_object = i;
				min_point_dist = point_dist; }}
    }
 
    setUiEvents();     
        
    document.body.oncontextmenu = function(){
        return false; };
   
    
    // draw(canvas);
    // var ctx = canvas.getContext('2d');
    // ctx.fillRect(0, 0, canvas.width, canvas.height);
    
    // 
    
    
    function animateLoop(){
        
		if(selected_points.length && !getKeyState(1002)){  // move selection if exists
			for(var i = 0; i < selected_points.length; ++i){
				var pt = points[selected_points[i]];
				vector_add(pt, delta_position, pt); }}
		
		else{
			moveCamera(delta_position, false); }
			
			
		if(!selected_points.length || getKeyState(16)){
			if(delta_horizontal_angle) rotateHorizontal(delta_horizontal_angle);
			if(delta_vertical_angle) rotateVertical(delta_vertical_angle); }
			
		else{ // rotate selected points
            var rotation_transform;
            if(selection_rotation_axis){  // 
                rotation_transform = vector_rotation(selection_rotation_axis, delta_horizontal_angle); }
            else{
                rotation_transform = vector_rotation([0, 1, 0], delta_horizontal_angle);
                rotation_transform = matrix_mult(vector_rotation([0, 0, 1], -delta_vertical_angle), rotation_transform); }

            for(var i = 0; i < selected_points.length; ++i){
                var index = selected_points[i];
                var pt = points[index];

                vector_minus(pt, rotation_center, pt);
                pt = matrix_mult(rotation_transform, [pt])[0];
                vector_add(pt, rotation_center, pt);
                points[index] = pt; }}
        
       
        //  overwrite point projections
        //  TODO this may not be perfect if points are deleted etc.        
        point_projections.length = 0;
        for(var i = 0; i < points.length; ++i){
            point_projections[i] = project(canvas, points[i], view_transform, origin); }
            
        line_midpoint_projections.length = 0;
        
        for(var i = 0; i < lines.length; ++i){
            var line = lines[i];
            if(!line){
                line_midpoint_projections[i] = null;
                continue; }
                
            var pt1 = point_projections[line[0]];
            var pt2 = point_projections[line[1]];
            if(pt1 && pt2){
                line_midpoint_projections[i] = [(pt1[0] + pt2[0])/2, (pt1[1] + pt2[1])/2]; }
            else{
                line_midpoint_projections[i] = null; }}
            

        draw(canvas);
    }
    setInterval(animateLoop, 1000/frame_rate);
    
    var saveAction = function(){
        localStorage.points = JSON.stringify(points);
        localStorage.lines = JSON.stringify(lines);
    }
        
    setInterval(saveAction, saveTimeout);
}
*/

</script>


<body></body>
</html>