i have made a view frustum from scratch in javascript. i have been having trouble with objects that the camera is place within and also facing away from.
an example of the problem is below
ive been stuck on this for months with little help, chatgpt recommended that i switch to a right handed corodinate system, so i did, but it didnt seem to fix the problem.
my proccess for putting pixels on the screen is described below
step 1 = cross product with camera matrix
step 2 = cross product with projection matrix
step 3 = divide co-ordinates by their own w co-ord (normalize w to 1)
step 3.5 = im currently skipping culling. culling would go here, but since my object is made of multiple vertices i cant cull a vertex just because its offscreen as it forms part of a whole object, and that would deform the rest of the object
step 4 = cross product with projection to screen matrix
below shows how the co-ordinates of 1 vertex changes with each step
co-ordinates of vertex before projection
[5.00,9.00,10.00,1.00]
good projection (tile is good)
step 1 = [5, 9, -4.799999999999979, 10]
step 2 = [-8.660254038143698, 15.588457268658656, 15.353535353535376, 4.799999999999979]
step 3 = [-1.8042195912799448, 3.2475952643039006, 3.198653198653217, 1]
step 4 = [-120.63293869199174, 637.1392896455851, 3.198653198653217, 1]
pixels = -120.63293869199174,637.1392896455851
#############################
bad projection from within the object (tile is deformed)
step 1 = [5, 9, 0.6000000000000014, 10]
step 2 = [-8.660254038143698, 15.588457268658656, 20.808080808080813, -0.6000000000000014]
step 3 = [14.433756730239462, -25.980762114431034, -34.68013468013461, 1]
step 4 = [2315.0635095359194, -3747.114317164655, -34.68013468013461, 1]
pixels = 2315.0635095359194,-3747.114317164655
###############################
bad projection from behind (tile appears on the ceiling, when A it should be on the floor and B it shouldn't be visible)
step 1 = [5, 9, 16.800000000000004, 10]
step 2 = [-8.660254038143698, 15.588457268658656, 37.17171717171718, -16.800000000000004]
step 3 = [0.5154913117942677, -0.9278843612296817, -2.2126022126022122, 1]
step 4 = [227.32369676914016, 10.817345815547753, -2.2126022126022122, 1]
pixels = 227.32369676914016, 10.817345815547753
does anyone know which step could be wrong or need changing in situation 2 and 3? and why?
below is a minimal (i know its 500 lines... but its about as minimal as i can get it) just open it in a browser and use wasd to control it.
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/11.3.3/math.js"></script>
<script async src="https://unpkg.com/es-module-shims#1.3.6/dist/es-module-shims.js"></script>
</head>
<body>
<div id="canvas div" style = "position: relative; left: 0px; float:left; top: 0px;" >
<h1> first person below </h1>
<canvas id="mi_canvas" width="300" height="300" style="border-style: solid;"></canvas> <br>
<h1> radar below </h1>
<canvas id="radar_canvas" width="300" height="300" style="border-style: solid;"></canvas>
</div>
<div id="big info div" style = "position: relative; left: 310px; float:left; float:top; top: 0px; width:400px;" >
<div id = "info_1111"> </div><br>
</div>
<script>
var floor_y_pos = 9
canvas = document.getElementById("mi_canvas");
ctx = canvas.getContext("2d");
radar = document.getElementById("radar_canvas");
radar_ctx = radar.getContext("2d");
render_distance = 1000;
fov = math.pi / 2
class Projection{
constructor(){
var NEAR = player.near_plane
var FAR = player.far_plane
var RIGHT = Math.tan(player.h_fov/2)
var LEFT = - RIGHT
var TOP = Math.tan(player.v_fov /2)
var BOTTOM = -TOP
var m00 = 2*NEAR / (RIGHT - LEFT)
var m02 = (RIGHT + LEFT)/(RIGHT - LEFT)
var m11 = 2*NEAR / (TOP - BOTTOM)
var m12 = (TOP + BOTTOM) /(TOP - BOTTOM)
var m22 = (FAR * NEAR) / (FAR - NEAR)
var m23 = -2 * NEAR * FAR / (FAR-NEAR)
this.projection_matrix = [
[-m00,0,m02,0],
[0,m11,0,0],
[m02,m12,-m22,-1],
[0,0,m23,0]
]
var HW=player.H_WIDTH
var HH = player.H_HEIGHT
this.to_screen_matrix = [
[HW,0,0,0],
[0,HH,0,0],
[0,0,1,0],
[HW,HH,0,1]
]
}
}
function multiply(a, b) {
var aNumRows = a.length, aNumCols = a[0].length,
bNumRows = b.length, bNumCols = b[0].length,
m = new Array(aNumRows); // initialize array of rows
for (var r = 0; r < aNumRows; ++r) {
m[r] = new Array(bNumCols); // initialize the current row
for (var c = 0; c < bNumCols; ++c) {
m[r][c] = 0; // initialize the current cell
for (var i = 0; i < aNumCols; ++i) {
m[r][c] += a[r][i] * b[i][c];
}
}
}
return m;
}
function mi_position_matrix_multiplier(A, B)
{
var new_matrix = []
for (var new_num_ind = 0; new_num_ind < A.length; ++new_num_ind)
{
this_num = 0;
for (var a_ind = 0; a_ind < A.length; ++a_ind)
{
this_num += (A[a_ind] * B[a_ind][new_num_ind])
}
// console.log("just added this num to my new matrix = "+this_num.toString())
new_matrix.push(this_num)
}
return new_matrix;
}
function pythagoras(thing1, thing2)
{
dist = (((thing1[0]-thing2[0])**2)+((thing1[1]-thing2[1])**2))**0.5
return dist
}
class vertex{
constructor(x, y,z , id){
this.id = id
this.position = [x,y,z,1]
this.min_dist = 1.5 // minimum possible distance between player and object
}
is_this_object_behind_player(){
var arrow_length = 0.0001;
var pointing_position = [player.position[0]+(player.forward[0]*arrow_length) , player.position[2]-(player.forward[2]*arrow_length)]
var dist1 = pythagoras([this.position[0],this.position[2]], pointing_position)
var dist2 = pythagoras([this.position[0],this.position[2]], [player.position[0],player.position[2]])
if (dist1 < dist2){
return true;}
else if (dist1 > dist2){
return false;}
else{console.log(" else ");}
}
screen_projection(){
var position = mi_position_matrix_multiplier(this.position , player.camera_matrix())
console.log(position+" = position , which is a cross product of this.position"+this.position+" & "+ player.camera_matrix()+ " = player.camera_matrix()")
update_matrix_info_debug("camera_matrix",player.camera_matrix())
update_matrix_info_debug("position", position)
position = mi_position_matrix_multiplier(position , projection.projection_matrix) // does this just convert the position to cameras reference frame.
console.log(position+" = position , which is a cross product of position"+position+" & "+ projection.projection_matrix+ " = projection.projection_matrix")
update_matrix_info_debug("projection_matrix",projection.projection_matrix)
update_matrix_info_debug("position after being multiplied by proj matrix", position)
// if so then i image to screen matrix is insufficient
for (let i = 0; i < position.length; i++) {
position[i] = position[i]/position[3]
}
console.log(position+" = position after being normaslized")
for (let i = 0; i < position.length; i++) {
if (i != 9787781){
console.log(i+" =-= "+position[i])
if (this.is_this_object_behind_player()){for (let ii = 0; ii < position.length; ii++) {position[ii] = -999999999;} console.log("culling1")}
if (position[i] > 2){for (let ii = 0; ii < position.length; ii++) {position[ii] = -9999;} console.log("culling2")}
if (position[i] < -2){for (let ii = 0; ii < position.length; ii++) {position[ii] = -9999;} console.log("culling3")}
}
} // also all examples say set position = 0 if culling
console.log(position+" = position after being culled")
position = mi_position_matrix_multiplier(position , projection.to_screen_matrix)
console.log(position+" = position after being multiplied by "+projection.to_screen_matrix+ " = projection.to_screen_matrix")
update_matrix_info_debug("projection.to_screen_matrix",projection.to_screen_matrix)
update_matrix_info_debug("position after being multiplied by projection.to_screen_matrix", position)
ctx.beginPath();
var scale_multiplier = (render_distance / pythagoras([this.position[0],this.position[2]] , [player.position[0],player.position[2]]))*1.5
var arrow_size = 0.02 * scale_multiplier;
ctx.moveTo(position[0]-arrow_size ,position[1]+arrow_size);
ctx.lineTo(position[0]+arrow_size ,position[1]-arrow_size);
ctx.moveTo(position[0]+arrow_size ,position[1]+arrow_size);
ctx.lineTo(position[0]-arrow_size, position[1]-arrow_size);
ctx.stroke();
}
return_screen_projection(dont_cull = false){
var position = mi_position_matrix_multiplier(this.position , player.camera_matrix())
position = mi_position_matrix_multiplier(position , projection.projection_matrix) // does this just convert the position to cameras reference frame.
for (let i = 0; i < position.length; i++) {
position[i] = position[i]/position[3]
}
position = mi_position_matrix_multiplier(position , projection.to_screen_matrix)
return [position[0],position[1]]
}
}
class player{
constructor(){
this.position =[0,0,0,1.0]
this.forward = [0,0,1,1]
this.up = [0,1,0,1]
this.right =[1,0,0,1]
this.h_fov = 3.1415926535/3
this.v_fov = this.h_fov * (canvas.height / canvas.width)
this.near_plane = 1
this.far_plane = 100
this.moving_speed = 0.2
this.rotation_speed = 0.1
this.H_WIDTH = canvas.width/2
this.H_HEIGHT = canvas.height/2
this.anglePitch = 0
this.angleYaw = 0
}
set_camera_angle(){
var rotate = multiply(rotate_x(this.anglePitch) , rotate_y(this.angleYaw))
this.forward = [0, 0, 1, 1]
this.up = [0, 1, 0, 1]
this.right = [1, 0, 0, 1]
this.forward = mi_position_matrix_multiplier(this.forward , rotate)
this.right = mi_position_matrix_multiplier(this.right , rotate)
this.up = mi_position_matrix_multiplier(this.up , rotate)
}
camera_yaw(angle){
this.angleYaw += angle}
translate_matrix(self){
var x = this.position[0];
var y = this.position[1];
var z = this.position[2];
var w = this.position[3];
return [
[1,0,0,0],
[0,1,0,1],
[0,0,1,0],
[-x,-y,z, 1]
]}
rotate_matrix(){
var rx = this.right[0]
var ry = this.right[1]
var rz = this.right[2]
var w = this.right[3]
var fx = this.forward[0]
var fy = this.forward[1]
var fz = this.forward[2]
var w = this.forward[3]
var ux = this.up[0]
var uy = this.up[1]
var uz = this.up[2]
var w = this.up[3]
return [
[rx,ux,fx,0],
[ry,uy,fy,0],
[rz,uz,fz,0],
[0,0,0,1]
]
}
camera_matrix(){
return multiply(this.translate_matrix(), this.rotate_matrix());
}
check_min_distance_isnt_overcome_by_this_move(dx, dy){
var can_move = true;
console.log(" zzzzzzzzzzzzz ")
for (let i = 0; i < objects.length; i++) {
var dist=Math.abs(pythagoras([objects[i].position[0], objects[i].position[2]] , [this.position[0], this.position[2]]))
var dist2=Math.abs(pythagoras([objects[i].position[0], objects[i].position[2]] , [this.position[0]+dx, this.position[2]+dy]))
console.log(dist +" ########################### " +dist2)
if ((dist2 < objects[i].min_dist)&(dist > dist2))
{can_move = false; console.log(objects[i].min_dist +" yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy")}
else{console.log(objects[i].min_dist+" can move this is bloody min dist xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx "+dist2);}
}
return can_move;
}
move(event)
{
var key_code = parseInt(event.keyCode)
if (key_code == 37 || key_code == 39 || key_code == 83 || key_code == 87 || key_code == 119|| key_code == 115)
{
var dx = Math.cos(this.angleYaw)*this.moving_speed
var dy = Math.sin(this.angleYaw)*this.moving_speed
console.log("that were moving = dx , dy = "+dx.toString()+" , "+dy.toString())
if ( key_code == 37 || key_code == 87 || key_code == 119) {
if (this.check_min_distance_isnt_overcome_by_this_move(dx, -dy)){
this.position[0] += -dy
this.position[2] += dx
}
}
if (key_code == 39 || key_code == 83 || key_code == 115) {
for (let i = 0; i < this.position.length; i++) {
if (this.check_min_distance_isnt_overcome_by_this_move(dx, dy)){
this.position[0] += dy
this.position[2] += -dx
}
}
}
}
else {
if ( key_code == 38 || key_code == 65 || key_code == 97) {
this.camera_yaw(-this.rotation_speed)
}
if (key_code == 40 || key_code == 68 || key_code == 100) {
this.camera_yaw(this.rotation_speed)
}
this.set_camera_angle()
}
}
}
function translate(pos){
tx,ty,tz=pos
return np.array([
[1,0,0,0],
[0,1,0,0],
[0,0,1,0],
[tx,ty,tz,1]
])}
function rotate_x(angle){
return [
[1,0,0,0],
[0,Math.cos(angle),Math.sin(angle),0],
[0,-Math.sin(angle),Math.cos(angle),0],
[0,0,0,1]
]
}
function rotate_y(a){
return [
[math.cos(a),0, -math.sin(a),0],
[0,1,0,0],
[math.sin(a), 0 , math.cos(a),0],
[0,0,0,1]
]
}
function update_radar(){
var arrow_length = 4;
var object_size = 6.5;
radar_ctx.beginPath();
var mid_screen = [radar.width/2,radar.height/2];
pointing_position = [mid_screen[0]+(player.forward[0]*arrow_length) , mid_screen[1]-(player.forward[2]*arrow_length)]
radar_ctx.moveTo(mid_screen[0], mid_screen[1]); // start of player pos on radar
radar_ctx.lineTo(pointing_position[0], pointing_position[1]);
radar_ctx.lineTo(pointing_position[0]-2, pointing_position[1]-2);
radar_ctx.lineTo(pointing_position[0]+2, pointing_position[1]+2);
radar_ctx.lineTo(pointing_position[0]-2, pointing_position[1]+2);
radar_ctx.moveTo(mid_screen[0], mid_screen[1]);
for (let i = 0; i < objects.length; i++) {
var dx = (player.position[0]-objects[i].position[0])
var dz = player.position[2]+objects[i].position[2]
var x = (dx*2) + mid_screen[0]
var z = (dz*2) + mid_screen[1]
x = x +(object_size/2)
z = z +(object_size/2)
radar_ctx.moveTo(x-object_size,z-object_size);
radar_ctx.lineTo(x+object_size,z+object_size);
radar_ctx.moveTo(x+object_size,z-object_size);
radar_ctx.lineTo(x-object_size,z+object_size);
}
radar_ctx.stroke();
}
function update_matrix_info_debug(matrix_name, matrix){
if (matrix[0].length > 1)
{
for (let x = 1; x < matrix.length+1; x++) {
for (let y = 1; y < matrix.length+1; y++) {
// console.log(matrix_name.toString()+"_"+x.toString()+y.toString());
document.getElementById(matrix_name.toString()+"_"+x.toString()+y.toString()).innerHTML = matrix[x-1][y-1]
}
}
}
else {
for (let x = 1; x < matrix.length+1; x++) {document.getElementById(matrix_name.toString()+"_"+"1"+x.toString()).innerHTML = matrix[x-1]}
}
}
class box{
constructor(x,z,size){
var low_y = 0.5
var high_y = low_y - size
this.position = [x+(size/2),0,z+(size/2)]
this.vertices = [new vertex(x,low_y,z,0),new vertex(x+size,low_y,z,1),new vertex(x,low_y,z+size,2),new vertex(x+size,low_y,z+size,3),
new vertex(x,high_y,z,4),new vertex(x+size,high_y,z,5),new vertex(x,high_y,z+size,6),new vertex(x+size,high_y,z+size,7)
]
this.faces=[ [0,1,3,2,0], [0,1,5,4,0] , [1,3,7,5,1] , [4,5,7,6,4] , [2,6,7,3,2] , [0,4,6,2,0]]
// this.faces=[ [0,4,6,2,0]]
}
draw_all_vertices(){
for (let i = 0; i < this.vertices.length; i++) {
this.vertices[i].screen_projection()
}
}
draw_all_faces(){
var each_point = []
for (let i = 0; i < this.vertices.length; i++) {
each_point.push(this.vertices[i].return_screen_projection())
}
var skip_drawing = if_most_of_these_numbers_are_off_screen(each_point)
if (skip_drawing){console.log(" skipp drawing any faces init ");return;}
ctx.fillStyle = '#f00';
var moved_to_first_yet = false
for (let face = 0; face < this.faces.length; face++) {
ctx.beginPath();
console.log("%%%%%%%%%%%%%%%%%%%%%%%%%");
console.log(this.faces);
console.log(this.faces[face]);
for (let vertex = 0; vertex < this.faces[face].length; vertex++)
{
console.log(vertex+" vertex bef dddddddddddddddddddddddddddddd")
var vertex2 = this.faces[face][vertex]
console.log(vertex2+" vertex aft ddddddddd ")
if (moved_to_first_yet == false)
{
moved_to_first_yet = true
ctx.moveTo( each_point[this.vertices[vertex2].id][0],each_point[this.vertices[vertex2].id][1]);
}
else{ctx.lineTo( each_point[this.vertices[vertex2].id][0],each_point[this.vertices[vertex2].id][1]);}
}
ctx.closePath();
ctx.fill();
}
}
}
class two_d_surdace {
constructor(verex1,verex2,verex3,verex4 , colour){
this.vertices = [verex1,verex2,verex3,verex4]
this.colour = colour
}
draw_all_faces(){
var each_point = []
for (let i = 0; i < this.vertices.length; i++) {
each_point.push(this.vertices[i].return_screen_projection(true))
}
ctx.fillStyle = this.colour;
var moved_to_first_yet = false
for (let vertex = 0; vertex < this.vertices.length; vertex++)
{
console.log(each_point[vertex][0]+" , "+each_point[vertex][1]+ " actual x y points on screen for this vertex of corner of floor ")
if (moved_to_first_yet == false)
{
moved_to_first_yet = true
ctx.moveTo( each_point[vertex][0],each_point[vertex][1]);
}
else{ctx.lineTo( each_point[vertex][0],each_point[vertex][1]);}
}
ctx.closePath();
ctx.fill();
}
}
function if_off_screen(x, y)
{
if (x> canvas.width || x < 0){console.log(x +" x = off screen "); return true;}
if (y > canvas.height || y < 0){console.log(y +" y = off screen "); return true;}
console.log(x +" , "+y + " =x,y = not off screen ");
return false;
}
function if_most_of_these_numbers_are_off_screen(numbers){
var threshold = 1; //Math.floor(numbers.length*0.49)
var counter = 0
console.log(numbers + " xxxx numbers as they come in ")
for (let i = 0; i < numbers.length; i++) { if (if_off_screen(numbers[i][0], numbers[i][1])){console.log(numbers[i]+" , "+numbers[i+1]+ " = numbers[i] are off screen"); counter +=1} else{console.log(numbers[i]+" , "+numbers[i+1]+ " = numbers[i] not off screen")} }
console.log("quuin quoirs of raptor");
if (counter >= threshold){console.log(threshold+" < " + counter);return true}
console.log(threshold+" > " + counter);
return false;
}
player = new player();
projection = new Projection()
objects = [] //
floor = new two_d_surdace(new vertex(50,floor_y_pos,50) , new vertex(-50,floor_y_pos,50) , new vertex(-50,floor_y_pos,-50) , new vertex(50,floor_y_pos,-50) , '#F90' )
update_radar()
$(document).on("keypress", function (event) {
player.move(event)
ctx.beginPath();
radar_ctx.beginPath();
radar_ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < objects.length; i++) {
objects[i].draw_all_faces()
objects[i].draw_all_vertices()
}
floor.draw_all_faces()
update_radar()
});
</script>
</body>
I beg you to help me. I'm creating a small chart and it's not working make the connecting lines between the rectangles so that they stretch
after moving the rectangle-nodes. What we have:
3 rectangles created in OOR js svg;
created connecting lines with rectangles (in OOP js svg);
function for moving rectangle-nodes.
What is the problem. When dragging nodes, the lines break away from the rectangles.
An example of how a diagram works
here
I ask for help in creating a function for updating the line when dragging the nodes of the diagram.
I think that it needs to put the value of the points of contact, put the name of the update function
to the code of the drag/drop function
created the updateLines() function;
inserted conn1.x1 into it; conn.y1;conn1.x2;conn1.y2;
inserted into it var connect1 = new Lines();
connect1.draw();
varx3 = rec3.location.x+100; var y3 = rec3.location.y; /////
var connect2 = new Lines(rec1.location.x+100,rec1.location.y+80, rec3.location.x+100, rec3.location.y,stroke='green',id='l2');
connect2.draw();
I inserted the name of the function into the makeDraggable(evt) code;
<style>
.background {
fill: #eee;
}
.static {
cursor: not-allowed;
}
.draggable {
cursor: move;
}
</style>
-----------------------
<svg xmlns="http://www.w3.org/2000/svg" id="forDraw" viewBox="0 0 1330 420" onload="makeDraggable(evt)">
</svg>
------------------
function makeDraggable(evt) {
var svg = evt.target;
svg.addEventListener('mousedown', startDrag);
svg.addEventListener('mousemove', drag);
svg.addEventListener('mouseup', endDrag);
function getMousePosition(evt) {
var CTM = svg.getScreenCTM();
return {
x: (evt.clientX - CTM.e) / CTM.a,
y: (evt.clientY - CTM.f) / CTM.d
};
}
var selectedElement, offset;
function startDrag(evt) {
if (evt.target.classList.contains('draggable')) {
selectedElement = evt.target;
offset = getMousePosition(evt);
offset.x -= parseFloat(selectedElement.getAttributeNS(null, "x"));
offset.y -= parseFloat(selectedElement.getAttributeNS(null, "y"));
}
}
function drag(evt) {
if (selectedElement) {
var coord = getMousePosition(evt);
selectedElement.setAttributeNS(null, "x", coord.x - offset.x);
selectedElement.setAttributeNS(null, "y", coord.y - offset.y);
}
}
function endDrag(evt) {
selectedElement = null;
}
}
var recWidth = '200';
var recHeight = '80';
function Rectangle(width,height, location={x: 400, y: 50},style='draggable', fill = 'red', stroke = 'green',id='ivan') {
this.width = recWidth;
this.height = recHeight;
this.location = location;
this.style = style;
this.fill = fill;
this.stroke = stroke;
this.id = id;
this.draw = function() {
forDraw.innerHTML += `<rect width="${this.width}" height="${this.height}" x="${this.location.x}" y="${this.location.y}"
class="${this.style}" fill="${this.fill}" stroke="${this.stroke}" id="${this.id}" />`;
}
}
Rectangle.prototype.draw = function() {
if (forDraw.getElementById(this.id)) forDraw.getElementById(this.id).remove();
forDraw.innerHTML += `<rect width="${this.width}" height="${this.height}" x="${this.location.x}" y="${this.location.y}"
class="${this.style}" fill="${this.fill}" stroke="${this.stroke}" id="${this.id}" />`;
}
var rec1 = new Rectangle();
rec1.draw();
var rec2 = new Rectangle(150,90, {x: 300, y:300}, style='draggable','yellow', 'red','petro');
rec2.draw();
var rec3 = new Rectangle(150,90, {x: 550, y:300}, style='draggable','green', 'blue','dima');
rec3.draw();
function Lines(x1=rec1.location.x+100, y1=rec1.location.y+80, x2=rec2.location.x+100, y2=rec2.location.y, stroke='blue',id='l1') {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
this.stroke = stroke;
this.id = id;
this.draw = function() {
forDraw.innerHTML += `<line x1="${this.x1}" y1="${this.y1}" x2="${this.x2}" y2="${this.y2}"
stroke="${this.stroke}" id="${this.id}" />`;
}
}
var connect1 = new Lines();
connect1.draw();
var x3 = rec3.location.x+100;
var y3 = rec3.location.y;
/////
var connect2 = new Lines(rec1.location.x+100,rec1.location.y+80, rec3.location.x+100, rec3.location.y,stroke='green',id='l2');
connect2.draw();