Show code
<think>
Okay, let's tackle this problem. The user wants an HTML file that uses a 2D canvas to display a rotating 3D height field. The grid should be built from a smooth mathematical function that changes over time. Each grid point needs to be projected in perspective, and the surface should rotate. Also, the mesh rows and columns should be drawn from back to front so that nearer parts cover the ones behind. Plus, we need to show the grid size and rotation angle, and it should loop with a new random instance each time. All without any libraries, just plain JS and Canvas 2D.
Hmm, first, I need to structure the HTML file. Since it has to be self-contained, the entire code will be in one HTML file. The canvas element should be there, and all the JavaScript inside script tags.
Starting with the canvas setup. The canvas element should have an id so we can reference it. Then, in the script, I'll get the canvas and its 2D context. The grid size can be a variable, maybe 20x20. The user mentioned showing grid size, so that variable should stay accessible.
Next, the rotation. The surface needs to rotate over time. So, we can track the current rotation angles for x and y axes. Maybe using angles that change per frame. The rotation should be smooth, so using requestAnimationFrame is essential here. The rotation angle will be displayed, so we'll need to update that in the canvas or in a separate element. Since it's a 2D canvas, maybe we can draw the text on the same canvas.
The height field is a grid of points, each with a z value determined by a function. The function needs to be smooth and time-dependent. Maybe something like combining sine waves or other periodic functions to create a smooth terrain. Let me think: for each point (x, y), the height could be a function of (x*t, y*t) where t is time, so it changes over time. Or maybe using a function that's a combination of sines with different frequencies.
Wait, for the height function, perhaps using a function like Math.sin(x * time + a) * Math.cos(y * time + b) where a and b are offsets. The time component will make it change over time. The parameters can be adjusted to control the smoothness and movement. Also, adding some randomness here might help in varying the terrain each time.
Now, the projection. Since it's a 3D to 2D projection in perspective, we need to apply some projection matrix. Since it's 2D context, we can't use WebGL matrices, so we have to handle the coordinates manually. Each point (x, y, z) needs to be transformed into 2D screen coordinates considering rotation and perspective.
The steps for each frame would be:
1. Clear the canvas.
2. Update the rotation angle based on time.
3. Calculate the current time (maybe using performance.now() or a counter).
4. For each grid point, compute its 3D position (x, y, z) where z is the height determined by the function.
5. Apply the rotation to the point (around some axis? Maybe x and y axes? The rotation might need to be around a central point, so translating to origin, rotating, then translating back).
6. Project the rotated 3D point to 2D using perspective. Perspective projection formulas: something like (x/(z + d), y/(z + d)) where d is a distance for perspective. Need to adjust the projection parameters for better visuals.
7. Sort the points by depth so that closer ones are drawn over farther ones. Since it's a grid, perhaps sorting rows based on their depth. Since the grid is 2D, maybe the depth can be determined by a function that depends on the rotated position. Alternatively, for each row or column, compute their average distance and sort them accordingly. Wait, the problem says "draw the mesh rows and columns from back to front". So, the mesh is made of lines connecting grid points, so perhaps we can iterate over the grid in order of depth, drawing each row and column in that order. But how to compute the depth for each row or column?
Alternatively, since it's a grid, each row (fixed y) or column (fixed x) can be sorted based on their z-coordinate? Or perhaps the z-coordinate after rotation? Hmm, this is tricky. Since the projection is perspective, the depth isn't straightforward. Maybe the depth can be determined by the distance from the viewer along the z-axis after rotation. So for each row (fixed y?), compute the z-coordinate of the center of the row, and sort rows by that z-coordinate. Then, draw the rows from back to front. Similarly for columns? Or maybe rows and columns are drawn in the order of their sorted depth.
Alternatively, perhaps each vertex's depth can be calculated, and then the entire grid is drawn in order from back to front. But that might be computationally intensive for a grid of 20x20. Maybe instead, for each row in the grid, compute a representative depth (like the average z-coordinate of the row's points after rotation) and sort the rows by that depth. Then, draw each row's lines in that order. Similarly for columns. But rows and columns are different, so maybe separate sorting for rows and columns? Or maybe columns are part of the rows?
Hmm, perhaps it's easier to render all rows and columns, but for each row (a horizontal line in the grid), we can process them from back to front. To do that, for each row (fixed y-coordinate in the original grid), compute its depth (distance from viewer), then sort all rows by this depth. Then, draw each row's lines in that order. Similarly for columns (fixed x-coordinate), determine their depth and sort them. But how precisely?
Alternatively, since the grid is a 2D array, for each cell (x, y), we can compute the projected 2D coordinates and then decide the order based on, say, the z-coordinate (after rotation but before projection). Because in perspective projection, the closer a point is along the camera's z-axis, the larger it appears, but for ordering, we can sort based on the negative z value (since lower z is closer? Wait, need to define the coordinate system).
Let me think about coordinate systems. Let me define the camera's position such that the rotation is around the center of the grid. So, each grid point (x, y, z) is relative to the center. Then, rotation matrices can be applied. The projection would take those rotated points and map them to 2D.
Suppose the grid is centered at (0,0,0). Each point's position is (dx, dy, dz), where dx ranges from -width/2 to width/2, similarly for dy. Then, applying rotation around the x and y axes, then projecting.
The projection formula could be:
After rotation, a point (x, y, z) is transformed. Then, perspective projection can be done by dividing by the z-component plus some distance (to avoid division by zero). So projected x and y would be something like:
projectedX = (rotatedX * (viewDistance + rotatedZ)) / (viewDistance)
Wait, maybe:
Let’s define a viewing distance D. The projected coordinates would be:
projectedX = (x / (rotatedZ + D)) * scale + canvasWidth/2
projectedY = (y / (rotatedZ + D)) * scale + canvasHeight/2 ?
Hmm, perhaps. But exact formulas might need adjusting. Alternatively, the perspective projection can be handled with a simple division based on the depth.
Alternatively, maybe using a simple orthographic projection with some perspective scaling. Let me look up the perspective projection equations.
Alternatively, the standard way is:
For a point in 3D space (x, y, z), rotated, then projected to 2D:
The projection matrix for perspective would involve dividing by the z coordinate. But since we have to do this manually, let's say:
After rotation, the point is (x, y, z). The screen coordinates are computed as:
screenX = (x * perspectiveDistance) / (z + perspectiveDistance) + centerX;
screenY = (y * perspectiveDistance) / (z + perspectiveDistance) + centerY;
Where perspectiveDistance is a parameter that controls the perspective effect. That might work. The idea is that as z approaches -perspectiveDistance, the projection goes to infinity; so we need to avoid that. So the Z component should be kept positive or at a certain distance.
Alternatively, perhaps I need to set up the coordinates so that the rotated Z is positive to see the objects. Let me think through the steps:
First, for each grid point (i, j):
- Compute its original coordinates. Let's say the grid is from -halfGridSize to +halfGridSize in both x and y directions. The grid has gridSize points, so each step is 1 unit (for simplicity). Wait, but the grid size is N x N where N is the grid size variable.
Wait, maybe it's better to represent the grid as a grid of points with indices from 0 to grid_size-1 in both x and y. The actual coordinates can be scaled so that the grid is centered and spaced properly.
Suppose grid_size is 20, so each grid point (i, j) has coordinates:
x = (i - (grid_size-1)/2) * scale
y = (j - (grid_size-1)/2) * scale
scale could be, say, 1 for simplicity, but might need to be adjusted based on the canvas size.
But the actual 3D coordinates would be (x, y, z), where z is the height.
The height z is given by some function f(x, y, t), where t is time. For example, z = Math.sin(x * 0.1 + t) * Math.cos(y * 0.1 + t) * amplitude. The parameters can be adjusted so that the height varies smoothly over time.
Then, apply rotation. The rotation is around the center of the grid, so first translate each point to the origin, rotate, then translate back?
Wait, no, the rotation is applied to the entire grid as it's rotating around some axes. Let's define two angles, angleX and angleY, which change over time. These angles control the rotation around the X and Y axes.
The rotation matrices can be applied to each point's coordinates (x, y, z).
Wait, actually, to rotate around the X axis:
The rotation matrix for an angle α around X is:
[1, 0, 0]
[0, cosα, -sinα]
[0, sinα, cosα]
Similarly, for rotation around Y axis with angle β:
[cosβ, 0, sinβ]
[0, 1, 0]
[-sinβ, 0, cosβ]
So, to apply both rotations, we need to multiply the two matrices. The order of rotations matters. Probably, first rotate around X, then Y, or vice versa.
Alternatively, apply a combined rotation. But for simplicity, perhaps the code can first rotate around X, then Y, or the other way. Let me think: perhaps the rotation angles are added incrementally each frame.
The rotation angles (angleX and angleY) are updated each frame by some delta. The delta is based on elapsed time, so that the rotation is smooth.
Once rotated, the points are projected onto 2D.
After applying the rotations, each point's new coordinates (x', y', z') will determine its position in screen coordinates.
The projection step is crucial here. To get perspective, we can use a formula similar to:
screenX = (x' * (distance + z')) / (distance + z') ? No, that might not be right. Wait, maybe:
Let me think of the projection as looking along the z-axis. The perspective projection can be done by dividing by the z component plus a distance. For example:
projectedX = (x' * perspectiveDistance) / (z' + perspectiveDistance) + centerX;
projectedY = (y' * perspectiveDistance) / (z' + perspectiveDistance) + centerY;
Where perspectiveDistance is a value that sets the distance of the virtual camera from the scene. This way, as z' increases (moving away), the division makes the projected coordinates smaller, giving perspective.
Alternatively, using a simpler approach where:
projectedX = (x' * (projectionFactor)) / (z' + projectionDepth)
But scaling and centering are important. The parameters need to be tuned so that the grid fits the canvas.
Now, the order of drawing is important. To have the back rows cover the front, we need to draw from back to front. Since each row (fixed j) is horizontal, perhaps we can sort rows by their depth (z-coordinate) and draw them in that order. Wait, but each row's depth varies across its columns. Alternatively, the depth for a row can be determined by the z-coordinate of its center point. For example, for row j, pick the central x-coordinate (the middle of the row) and compute the z value there. Then, sort rows based on their z value. The rows with lower z (further away) are drawn first, and higher z (closer) are drawn later, covering the previous ones.
Alternatively, since the projection includes z', maybe the depth is determined by the z' coordinate after rotation but before projection. Or perhaps the distance from the viewer can be estimated by the negative z' component (assuming that the viewer is looking along the negative z' axis). So, for each point, the depth is 1/(z' + D), where D is a constant to prevent division by zero. To order rows, we can compute an average depth for the row and sort the rows accordingly.
Hmm, perhaps for each row (fixed y in the grid), compute the average z' of the points in that row, then sort the rows by the z' such that rows with lower z' (farther) come first. But how exactly?
Alternatively, maybe sort each row by the sum of the z-components of its points, and sort in order from lowest to highest. The rows with the lowest sum are drawn first.
Alternatively, since each row's points are along a horizontal line, the middle point might be representative of the row's depth. So, for each row j, compute the z-coordinate of the center point (i = grid_size/2), then sort the rows based on that z-coordinate. The rows with lower z (farther) are drawn first.
This might work as an approximation.
Once the rows are sorted, we can loop through them in order and draw each row's horizontal line and vertical lines (columns) for that row.
Wait, but the problem says to draw both rows and columns. So, each row is a horizontal line between its left and right points, and each column is a vertical line between its top and bottom points. So, for each row, after sorting, we can draw all its horizontal lines, and then also draw its vertical lines? Or perhaps draw all rows first in sorted order, then all columns? No, because columns also need to be ordered depth-wise.
Alternatively, for both rows and columns, sort them based on their depth and draw in order. But rows and columns are different, so perhaps process rows first, then columns in sorted order. Or maybe process each row and column together.
Hmm, perhaps it's better to process rows first in the sorted order, drawing their horizontal lines, and then process columns in their own sorted order, drawing their vertical lines. But need to ensure that columns are also ordered back to front.
Alternatively, the entire grid's lines can be considered as a collection of lines, each with their average depth, and sorted all together. But that might be more complex.
Alternatively, for the rows, sorted by row depth, and for each row, draw all horizontal lines (each row is one horizontal line per row?), then draw the columns (vertical lines) in their own sorted order.
Alternatively, perhaps the rows can be drawn in order, which takes care of horizontal lines, and then the columns can be drawn in their own order, sorted by their own depth. But this might have some overlapping issues.
Hmm, this part is a bit tricky. The user instruction says "draw the mesh rows and columns from back to front so nearer rows cover the ones behind them." So the rows and columns must be drawn in such an order that the closer lines are on top.
Perhaps the rows are drawn from back to front, so each row's horizontal lines are drawn, with later rows (closer) overlapping the earlier ones. The columns, which are vertical lines, need to be drawn similarly. But how to handle their depth?
Maybe the columns can be sorted individually, but that's time-consuming. Alternatively, for the columns, since they are vertical, their depth can be determined by their central point's depth. So for each column i, compute the center point's depth, sort the columns in order, and draw them similarly.
Alternatively, since rows and columns are separate, perhaps the rows are drawn first in row order, columns are drawn in column order, but each column is sorted based on their depth. But that might require more computational steps.
Alternatively, maybe the code can sort rows by their depth and draw their horizontal lines in that order. Then, for the columns, sort them by their depth and draw their vertical lines in that order. The key is that all horizontal lines from a row are drawn before or after the columns, depending on which is closer.
Alternatively, process each row, drawing its horizontal lines, and also its vertical lines (columns) in the correct order. Wait, but columns span multiple rows, so that complicates things.
Alternatively, perhaps the code should process each row in order, and for each row, draw its horizontal line and then draw the vertical lines (columns) at each point. Wait, but vertical lines are part of the columns. Hmm, this is getting too tangled.
Maybe the simplest approach is to first draw all the horizontal lines (rows) in sorted order, and then draw all vertical lines (columns) in their own sorted order. Since columns are vertical, their depth might depend on their x-coordinate. Let's say for each column, the depth is the average of all points along that column's z after rotation. Then, sort the columns by that depth and draw them in order.
This way, rows are drawn in row order, covering appropriately, and then columns are drawn in column order, covering appropriately.
Alternatively, the columns could be part of the rows, so drawing the rows first (horizontal lines) and then the columns (vertical lines) in their own order. The columns might overlap with previous rows but since they are drawn later, they would be on top where they should be.
Alternatively, perhaps the code can process the entire grid, and for every line segment (row or column), compute the average or some representative depth, and then sort all line segments from back to front and draw them in that order. But that would involve a lot of lines and sorting.
Alternatively, since the rows are horizontal and columns vertical, maybe the rows and columns can be sorted independently as two separate groups.
Let me try to outline the steps:
1. Compute the rotation angles based on time.
2. For each point (i,j) in the grid:
a. Compute its 3D coordinates (x, y, z).
b. Apply rotation to get (x', y', z').
3. For each row (fixed j):
a. Compute the depth of the row (e.g., average z' of all points in the row, or midpoint's z').
b. Sort rows in order from farthest (lowest z') to closest (highest z').
4. Draw all rows (horizontal lines) in this sorted order.
a. For each row j:
i. Find the two endpoints (leftmost and rightmost points in the row).
ii. Project these endpoints to screen coordinates.
iii. Draw a line between them.
5. For each column (fixed i):
a. Compute the depth of the column (e.g., average z' of all points in the column, or midpoint's z').
b. Sort columns in order from farthest to closest.
6. Draw all columns (vertical lines) in this sorted order.
a. For each column i:
i. Find the two endpoints (top and bottom points in the column).
ii. Project to screen coordinates.
iii. Draw a line between them.
This way, rows are drawn first in order, then columns in their own order. However, there may still be overlaps where columns are drawn on top of rows or vice versa, but since rows are drawn first and columns later, the columns will be on top. But columns might need to be behind some parts. Hmm.
Alternatively, perhaps the order should be columns first then rows? Not sure.
Alternatively, separate the processing of horizontal and vertical lines, but ensure that lines closer to the viewer are drawn later, thus on top. So rows sorted back to front, columns sorted back to front.
Alternatively, perhaps the rows and columns should be all considered as line segments, sorted by their depth, and drawn in order. However, this would require sorting all line segments, which might be computationally intensive but manageable for a grid of 20x20.
Wait, for a grid of 20x20, there are 20 rows (each row is 20 points but each row has 19 horizontal lines between points? Or each row is a single line connecting the first and last point of the row?). Wait, the problem says "mesh rows and columns", so likely each row is a horizontal line connecting all points in the row, and each column is a vertical line connecting all points in the column. So each row is a single horizontal line segment per row? No, each row would consist of horizontal lines between consecutive points along the row? Or is each row's horizontal line the full row from one side to the other?
Probably, each row is a single horizontal line spanning the entire row, so for row j, the line connects the first and last point of the row. Similarly for columns.
So, for rows: there are N rows (each with N points), so N horizontal lines, each spanning the entire row.
Similarly, there are N columns (each with N points), so N vertical lines, each spanning the entire column.
Therefore, there are 2*N lines total. So sorting all of them and drawing in order is feasible.
So the approach would be:
- For each row line:
- Compute its depth (e.g., average z' of all points in the row, or the midpoint's z').
- For each column line:
- Compute its depth similarly.
- Combine all lines (rows and columns) into a list.
- Sort the list from back to front based on depth.
- Draw each line in sorted order.
This way, all lines are drawn correctly, with closer lines on top.
This is probably the most accurate method, though it requires some computation.
Now, moving to coding steps:
Variables needed:
- grid_size (like 20)
- center_x and center_y to compute screen coordinates.
- rotation angles (angleX and angleY), which are updated each frame.
- time-based delta for smooth rotation.
Projection parameters:
- perspectiveDistance (distance from camera to projection plane?)
- scaling factors to fit the grid into the canvas.
Now, in pseudocode:
Initialize grid_size and other parameters.
In setup():
canvas size, set up context.
In draw():
clear canvas.
compute current angleX and angleY based on time.
create a list of all line segments (rows and columns).
for each row j in 0..grid_size-1:
compute all points of row j, apply rotation, get their (x', y', z').
compute the depth of row j (e.g., average z' or midpoint's z').
create a row_line object with endpoints (first and last projected points) and depth.
for each column i in 0..grid_size-1:
compute all points of column i, apply rotation...
compute depth similarly.
create column_line object with endpoints (top and bottom projected points) and depth.
combine all lines into a list.
sort the lines by depth (back to front): from lower z' (farther) to higher z' (closer). So sort in ascending order of depth.
for each line in sorted list:
draw the line between its endpoints.
But how exactly to compute the projected endpoints?
Wait, each line is a horizontal row or vertical column line. For example, a row j has points from i=0 to i=grid_size-1, so the endpoints are the first and last points in the row. To find their projected screen coordinates, we can compute their (x', y', z') after rotation, then project them using the projection formula, then convert to screen coordinates.
So, for each row j:
first point is (i=0, j), last point is (i=grid_size-1, j)
compute their rotated coordinates and project.
Similarly for columns.
Alternatively, to compute the endpoints' screen positions:
For a row, the two endpoints are (min_x, y_j) and (max_x, y_j). But in 3D coordinates, the actual coordinates depend on the grid's layout.
Wait, perhaps first define the 3D coordinates for each point:
Let's define the grid points as follows:
Each point (i, j) has:
x = (i - (grid_size-1)/2) * scale_x
y = (j - (grid_size-1)/2) * scale_y
z = height_function(i, j, current_time)
Wait, but the height is a function over x and y, so maybe:
x and y are the grid's local coordinates. To simplify, we can set scale_x and scale_y to 1, so the grid spans from -grid_size/2 to +grid_size/2 in x and y dimensions.
Then, the height z can be something like:
z = Math.sin(x * 0.2 + t) * Math.cos(y * 0.2 + t) * amplitude;
where t is time in seconds.
But the exact function is up to the user, as long as it varies smoothly.
The rotation is applied to each point (x, y, z):
First rotate around X axis by angleX, then around Y axis by angleY.
The rotation matrices:
For X rotation:
x' = x
y' = y * cos(angleX) - z * sin(angleX)
z' = y * sin(angleX) + z * cos(angleX)
For Y rotation:
x'' = x' * cos(angleY) + z' * sin(angleY)
y'' = y'
z'' = -x' * sin(angleY) + z' * cos(angleY)
So the total rotation is first X then Y.
Then, after both rotations, the coordinates (x'', y'', z'') are the rotated coordinates.
Then, we project:
// perspective projection: assuming a camera looking along the negative Z axis.
// The projection can be done using:
// screenX = (x'' * perspectiveDistance) / (z'' + perspectiveDistance) + centerX
// screenY = (y'' * perspectiveDistance) / (z'' + perspectiveDistance) + centerY
Wait, but need to ensure that z'' + perspectiveDistance is not zero. The perspectiveDistance is a positive constant (e.g., 100).
Alternatively, maybe use a different projection formula. Let me think.
Let me set the projection parameters:
Let’s set the perspective distance (camera to projection plane) as a fixed value, say 100.
The screen coordinates can be derived as follows:
Let’s define the screen width and height as canvas dimensions.
Let’s assume the canvas has width=canvasWidth, height=canvasHeight.
The center of the screen is (canvasWidth/2, canvasHeight/2).
The projection formulas could be:
// After rotation, the coordinates are (x_rot, y_rot, z_rot)
// Perspective projection:
var factor = perspectiveDistance / (z_rot + perspectiveDistance);
screenX = centerX + x_rot * factor;
screenY = centerY + y_rot * factor;
This way, as the object moves away along the Z axis (z_rot increases), factor decreases, making the projected points smaller.
Wait, but if the object is behind the camera (z_rot < -perspectiveDistance), then division by zero / negative can occur. To avoid that, we can clamp z_rot + perspectiveDistance to be positive.
Alternatively, we need to ensure that for our grid, the z_rot values don't go too negative.
Alternatively, perhaps use a simple orthographic projection for simplicity? But the user asked for a 3D effect so perspective is better.
Alternatively, maybe use a different projection approach.
Alternatively, to make it simple for now, let's assume:
We can first apply rotation, then project using:
screenX = (x'' * projectionScale) + centerX;
screenY = (y'' * projectionScale) + centerY;
But this is orthographic. To add perspective, perhaps:
screenX = (x'' / (z'' + perspectiveDistance)) * scale + centerX;
screenY = (y'' / (z'' + perspectiveDistance)) * scale + centerY;
But the exact implementation requires some constants.
In any case, the key steps are:
1. For each point, compute rotated coordinates.
2. Project to 2D screen coordinates with perspective.
Now, putting this into code:
First, set up some variables:
const grid_size = 20;
const centerX = canvas.width/2;
const centerY = canvas.height/2;
const perspectiveDistance = 200; // arbitrary value to control perspective effect
const projectionScale = 50; // scaling factor for the projection.
var angleX = 0;
var angleY = 0;
const rotationSpeed = 0.01;
var lastTime = 0;
function animate() {
const time = performance.now();
const deltaTime = time - lastTime;
lastTime = time;
angleX += rotationSpeed * deltaTime;
angleY += rotationSpeed * deltaTime;
// compute all points
var lines = [];
// compute rows and columns
for(var j=0; j<grid_size; j++) {
// get first and last points of row j
var rowPoints = [];
var firstPoint = getProjectedPoint(0, j);
var lastPoint = getProjectedPoint(grid_size-1, j);
// create a line object for the row
lines.push({
type: 'row',
start: firstPoint,
end: lastPoint,
depth: (firstPoint.z + lastPoint.z)/2 // average z?
});
}
for(var i=0; i<grid_size; i++) {
var firstPoint = getProjectedPoint(i, 0);
var lastPoint = getProjectedPoint(i, grid_size-1);
lines.push({
type: 'column',
start: firstPoint,
end: lastPoint,
depth: (firstPoint.z + lastPoint.z)/2
});
}
// sort lines by depth (back to front)
lines.sort( (a,b) => a.depth - b.depth );
// clear canvas and draw lines
ctx.clearRect(0,0,canvas.width, canvas.height);
for(var line of lines) {
ctx.beginPath();
ctx.moveTo(line.start.x, line.start.y);
ctx.lineTo(line.end.x, line.end.y);
ctx.stroke();
}
requestAnimationFrame(animate);
}
function getProjectedPoint(i,j) {
// compute 3D coordinates
var x = (i - (grid_size-1)/2) * 10; // scale_x = 10
var y = (j - (grid_size-1)/2) * 10;
var z = Math.sin(x * 0.1 + angleX) * Math.cos(y * 0.1 + angleY) * 20;
// rotate around X and Y axes
var angleX_rad = angleX * (Math.PI/180); // assuming angles are in degrees?
var angleY_rad = angleY * (Math.PI/180);
// rotate X first
var y1 = y * Math.cos(angleX_rad) - z * Math.sin(angleX_rad);
var z1 = y * Math.sin(angleX_rad) + z * Math.cos(angleX_rad);
// rotate Y
var x1 = x * Math.cos(angleY_rad) + z1 * Math.sin(angleY_rad);
var z2 = -x * Math.sin(angleY_rad) + z1 * Math.cos(angleY_rad);
// now x1, y1, z2 are rotated coordinates
// perspective projection
var factor = perspectiveDistance / (z2 + perspectiveDistance);
var screen_x = centerX + x1 * factor * projectionScale;
var screen_y = centerY + y1 * factor * projectionScale;
// return the projected coordinates and the z value (for depth sorting)
return {
x: screen_x,
y: screen_y,
z: z2 // using z2 as depth (since lower z2 means closer?)
};
}
Wait, but here the depth sorting is based on z2, which is the rotated Z coordinate after both rotations. However, the perspective is based on z2. The depth for sorting should be based on the actual distance from the camera. For perspective projection, the depth can be represented by the z2 coordinate, but since we are using a perspective formula, the actual depth in 3D space is z2. To sort back to front:
- Objects with lower z2 (closer to the camera, since if z2 is negative, they are in front of the projection plane).
Wait, perhaps the depth for sorting should be the negative of z2, so that closer objects (lower z2) are drawn later.
Wait, let's think:
Suppose z2 is the rotated Z coordinate.
- If z2 is very large (far away), the projected points will be small and towards the center.
- If z2 is negative (behind the projection plane?), but depending on the perspectiveDistance.
Wait, perhaps it's better to compute the actual distance from the camera, but that might complicate.
Alternatively, the depth for sorting can be the z2 value, but since in the perspective formula, the projected position depends on 1/(z2 + perspectiveDistance). So if we want to sort from far to near (back to front), we can sort based on increasing z2 + perspectiveDistance. Because when z2 is small (closer), z2 + perspectiveDistance is smaller, so the object is closer.
Wait, no:
Wait, if z2 is the distance along the Z axis after rotation.
Assuming the camera is at (0,0,0), looking along the negative Z axis.
Thus, the objects with higher z2 (more in the positive Z direction) are farther away.
Thus, to sort back to front, we should sort the lines in increasing order of (z2 + perspectiveDistance). Because:
- A point with z2 = 10 is farther than z2 = 5.
Thus, to draw back to front, sort lines by their z2 in ascending order.
Hence, in the depth field of the line objects, we can set depth = z2, and sort in ascending order.
Thus in getProjectedPoint(), the z value returned is z2.
Then in the line object, depth is the average of the two endpoints' z2.
Wait, but for a row line, the two endpoints may have different z2 values. So the average might not be the best. Alternatively, take the average of the midpoint's z2? Or perhaps compute the midpoint's coordinates.
Alternatively, compute the midpoint in 3D space and use its z2 as the depth for the line.
Alternatively, to simplify, take the average of the two endpoints' z2.
In the code above, for a row line, the depth is (firstPoint.z + lastPoint.z)/2.
Similarly for columns.
Thus, with this setup, the lines will be sorted by their depth, and drawn from back to front.
However, there might be some artifacts because the lines are straight and the depth is an average, but it should give a reasonable result.
Testing this code might require adjusting the perspectiveDistance and projectionScale to get a good visual effect.
Also, note that the initial rotation angles may be in degrees, but in JavaScript Math functions use radians, so the angleX and angleY variables in the getProjectedPoint() function are converted to radians.
Wait, in the animate() function, are angleX and angleY in degrees or radians?
The code currently has:
var angleX = 0;
var angleY = 0;
The rotationSpeed is 0.01, which if it's degrees per millisecond, but multiplied by deltaTime (milliseconds), so:
angleX += rotationSpeed * deltaTime;
If rotationSpeed is in degrees per millisecond, then with 0.01 degrees per millisecond, over 1 second (1000 ms), it would rotate 10 degrees.
Alternatively, maybe it's better to use radians.
Perhaps better to use radians for all angle calculations.
Let me adjust:
Set rotationSpeed in radians per millisecond:
const rotationSpeed = 0.0003; // e.g., 0.18 degrees per millisecond.
But maybe better to compute the delta in seconds.
Wait, the deltaTime is in milliseconds, so converting to seconds:
deltaTime_seconds = deltaTime / 1000;
Thus:
angleX += rotationSpeed * (deltaTime/1000);
where rotationSpeed is in radians per second.
Thus, setting rotationSpeed to 0.1 radians per second (approx 5.7 degrees/s):
const rotationSpeed = 0.1; // radians per second
Then:
angleX += rotationSpeed * (deltaTime / 1000);
Similarly for angleY.
This way, the rotation speed is properly in radians per second.
This adjustment is important for correct rotation.
So revising the variables and functions accordingly.
Also, need to ensure that the projectionScale and perspectiveDistance are set such that the grid fits on the canvas.
Probably need to experiment with the parameters.
Additionally, in getProjectedPoint(), the X and Y coordinates are computed as:
var x = (i - (grid_size-1)/2) * 10;
This spreads the grid out over a 10 units per grid step, so for grid_size=20, the maximum i is 19, so:
x ranges from (- (19 - (20-1)/2 )) ?
Wait, for i=0:
x = (0 - (19)/2)*10 = (-9.5)*10 = -95
For i=19: x = (19 - 9.5)*10 = 9.5 *10= 95
Thus, the grid extends from -95 to +95 in x and y.
The projectionScale and perspectiveDistance need to be set so that the projected points fit the canvas.
Suppose the canvas is 800x600:
centerX = 400, centerY = 300.
projectionScale: higher value would make the grid larger on the screen.
perspectiveDistance: controls how much perspective is applied. A smaller value gives a stronger perspective effect.
Perhaps start with:
perspectiveDistance = 200,
projectionScale = 3.
But need to test.
Also, in the z calculation:
var z = Math.sin(x * 0.1 + angleX) * Math.cos(y * 0.1 + angleY) * 20;
This creates a wave-like pattern in the z coordinate, which adds some 3D effect.
Alternatively, using a different function for z, such as a heightmap.
Overall, the code should create an animated 3D grid with rotating perspective, where lines are drawn back to front based on their depth.