Show code
<think>
Okay, I need to create a self-contained HTML file that simulates flying through a starfield using a 2D canvas. The user specified a strict limit of 7000 characters, so I have to keep everything concise. Let me think through the steps.
First, the structure: the HTML file needs a canvas element and some script. I'll start with the basic HTML5 template. The canvas should have an id, maybe "c", and a context. Since it's a strict limit, I need to minify code as much as possible.
Next, the stars. They need x, y, z coordinates. To make them move towards the viewer, I'll decrement their z each frame. When a star's z becomes negative (i.e., passed the camera), I'll reset its z to a large positive value and randomize x and y. The projection uses perspective: x' = x * (1/z), y' = y * (1/z). Wait, actually, the perspective divide is usually done by scaling with 1/z. But I need to adjust for screen coordinates. Maybe scale by some factor to make it visible.
The stars should appear brighter and larger as they approach. So, their size and brightness can be proportional to 1/z. Since z is getting smaller (closer), 1/z increases. But when z is negative (past the camera), we need to reset. Hmm, but when z approaches zero, the star would be very bright and big. Maybe use 1/z squared for brightness to make it more intense?
For the stars' coordinates, I need to initialize them with random x and y between -width/2 and width/2, and z between some max value. Let's say for each star, x and y are within a range that when divided by z (starting at say 100) would project to the screen. The projection formula might be something like (x * (depth / z) + centerX), same for y. Wait, perhaps I need to use 1/z as the scaling factor. Let me think of the math again.
The actual projection is a bit tricky. To simulate depth, moving from z far away (large z) to closer (smaller z). The screen coordinates would be (x * (1/z), y * (1/z)), but scaled to the canvas size. Maybe set a viewing distance, so the scaling factor is (view_distance / z). Let's say view_distance is 100. So when z is 100, the scaling is 1, so x and y are as given. When z is smaller, the scaling increases, making the star bigger. But when z is 0, it would be infinite, which is bad, so we reset when z reaches 0 or goes negative.
Wait, actually, the z starts at a large value (say 100) and decreases each frame. The speed is the amount we subtract from z each frame. The star moves towards the viewer. When z becomes negative, we reset it to a large value again, and assign new x and y.
So for each star:
Initialize z to a random number between, say, 100 and 200? Or maybe all start at higher z, but with different initial positions.
The position on the canvas can be calculated as:
cx = (x/z) * viewScale + centerX;
cy = (y/z) * viewScale + centerY;
viewScale is a scaling factor to adjust how much the stars spread out. Maybe 500?
The radius of the star can be something like (200/(z)), but capped at a maximum size. Wait, but when z is small (like 10), that gives 20, maybe. Or maybe 5*(viewScale / z). Hmm.
Also, brightness: the color can be a gradient from white to yellow? Or just vary the alpha and size. Alternatively, set the stroke and fill opacity based on 1/z. But using fillStyle with rgba might help.
Now, for the code structure:
1. Initialize the canvas, get its context.
2. Define variables for canvas width and height, which should be the same as the actual canvas size (maybe using clientWidth/clientHeight to get the browser's viewport size? Or set it directly?)
Wait, the user might want it to be full screen? Or just a default size. The problem says "show the star count and current speed", so maybe a small canvas in the page with some text.
Hmm, the problem says "strict limit: max 7000 characters". So I should make the canvas as small as possible. Maybe 640x480? Or even smaller. Let's see.
Wait, the code will have to be as compact as possible. Let me think of variables:
- stars array, each with x, y, z.
- parameters like count (number of stars), speed (how much z decreases each frame).
- canvas size (width and height).
In JavaScript:
Initialize variables:
var c = document.getElementById('c'), ctx = c.getContext('2d'), w = 640, h = 360, count = 500, speed = 2;
Wait, the user said "a few hundred stars". Maybe 500 is okay. But if that's too much, maybe reduce? The speed can be adjusted.
But the problem mentions "show the star count and the current speed". So in the drawing loop, we need to display those. So text at the corners.
Now, the stars array:
We need to initialize each star with x, y, z. Let me see, initializing could be done in a loop:
var stars = [];
for (let i = 0; i < count; i++) {
stars.push({
x: Math.random() * 500 - 250, // random between -250 and 250?
y: Math.random() * 500 - 250,
z: Math.random() * 100 + 200 // initial z between 200 and 300, so far away
});
}
Wait, but maybe using a different range for x and y. Alternatively, maybe using a radius for the initial position on a sphere around the camera, but in 2D maybe just random x/y within a square.
Alternatively, since the z starts at some large value, the initial x and y can be within a range so that when divided by z, their projection is within the screen. For example, if z is 200, then x and y could be up to 200 in magnitude, so when divided by 200 gives up to 1, multiplied by some scaling factor. Hmm.
Alternatively, maybe set x between -w/2 and w/2, but scaled by some factor. Wait, perhaps the initial x and y can be in a square that's scaled by the initial z so that their projection is visible but not overlapping too much. Maybe I need to think of the initial x and y as within a certain range relative to their starting z.
Hmm, perhaps the initial x and y can be between -z and +z. But that might complicate things. Alternatively, just use random values between, say, -100 and +100 for x and y, and z starts at 200.
But maybe the initial z can be between 100 to 300. So their initial x and y can be up to 100, so when divided by z (say 100), gives a maximum of 1 (times scaling factor). So scaling factor could be like 100, so that 1 * scaling gives 100 pixels from center, so the stars start offscreen.
Alternatively, the scaling factor could be something like 1000 divided by the initial z. Hmm, perhaps the way to go is to use the following projection:
cx = (x * (1/z)) * scale + centerX;
Similarly for y.
The scale can be a constant to adjust how much they spread out.
Let me think of the code steps for each star in the animation loop:
For each star:
- reduce z by speed: z -= speed;
- if z < 0: reset x, y, z to new random values.
Then project x and y to screen coordinates:
var scale = 500;
cx = (x / z) * scale + c.width/2;
cy = (y / z) * scale + c.height/2;
The radius can be something like (scale / z) * 2, so that as z decreases, the radius increases. But when z is small, the radius becomes big, so maybe cap it. Alternatively, let it go, but when z is too small, it becomes a big dot. Hmm.
The fillStyle can be set to a white color that's brighter as the star approaches. Maybe the alpha is (1/z) * 200, but clamped at 1.0? Or use the radius as a factor for the color intensity.
Alternatively, using fillStyle as 'rgba(255,255,255,' + (1/z) + ')' but since 1/z can exceed 1 when z <1, maybe clamp it to 1.
Wait, when z is 0.5, 1/z is 2, which would be alpha 2, but that's invalid. So maybe (1/z)*0.5, but that depends on the scale. Hmm, perhaps better to use Math.min(1/z * 0.5, 1) ?
Alternatively, setting the radius and alpha based on 1/z.
Putting this together, the step for each star would be:
ctx.fillStyle = 'rgba(255,255,255,' + (1/z * 0.5) + ')';
ctx.beginPath();
ctx.arc(cx, cy, 2 * (scale/z), 0, Math.PI*2);
ctx.fill();
Wait, but doing this for 500 stars each frame might be slow. Wait, but requestAnimationFrame is okay for that.
Wait, let's see:
The steps for the animation loop:
- Clear the canvas.
- Iterate over all stars:
For each star:
z -= speed;
if (z < 0) {
reset x, y, z to new random values.
}
compute cx, cy based on x, y, z.
draw the star as a circle with position and radius.
- Draw the text with star count and speed.
So in code:
function loop() {
ctx.clearRect(0,0,w,h);
for (let i = 0; i < count; i++) {
let s = stars[i];
s.z -= speed;
if (s.z < 0) {
s.x = Math.random() * 400 - 200; // random x between -200 and 200 (assuming scaling factor makes it on screen)
s.y = Math.random() * 300 - 150; // y between -150 and 150, depending on aspect ratio
s.z = Math.random() * 100 + 200; // new z between 200 and 300.
}
let scale = 500; // choose a value that makes the stars spread out.
let cx = (s.x / s.z)*scale + w/2;
let cy = (s.y / s.z)*scale + h/2;
let rad = (scale / s.z)*2;
ctx.fillStyle = 'rgba(255,255,255,' + (rad/20) + ')'; // brightness based on radius?
ctx.beginPath();
ctx.arc(cx, cy, rad, 0, Math.PI*2);
ctx.fill();
}
ctx.fillStyle = 'white';
ctx.fillText('Stars: '+count+' Speed: '+speed, 10, h-10);
requestAnimationFrame(loop);
}
Wait, but the initial setup needs to create the stars array.
Putting all together:
In the HTML, the canvas element needs to be set with width and height, maybe via attributes.
Wait, in HTML, the canvas can be set to width and height, so:
<canvas id="c" width="640" height="360"></canvas>
Then, in the script:
var c=document.getElementById('c'),ctx=c.getContext('2d'),w=c.width,h=c.height;
count=500; speed=2;
stars=[];
for(let i=0;i<count;i++) stars.push({x:Math.random()*400-200,y:Math.random()*300-150,z:Math.random()*100+200});
function loop(){...}
loop();
Wait, but I need to start with requestAnimationFrame.
Wait, in the code:
window.requestAnimationFrame(loop);
But the loop function calls itself with requestAnimationFrame.
So the code steps:
Initialize variables.
Then define loop function.
Then start with requestAnimationFrame(loop).
Now, variables:
The count and speed are constants? Or can the user change them? The problem says "show the current speed" but doesn't mention changing it, so probably just display the initial value.
Wait the problem says "current speed", so maybe speed is a variable that could be adjusted, but since there's no user input, it's probably a constant. So just display the initial speed value.
Putting all together, I'll need to write the HTML file with all this code in a compact way, removing as much whitespace as possible.
Now, possible optimizations to save characters:
- Use one-letter variables where possible.
- Inline code as much as possible.
- Combine declarations.
For example, instead of:
var c=document.getElementById('c'),ctx=c.getContext('2d'),w=c.width,h=c.height;
count=500;speed=2;
stars=[];
Could be written as:
var c=document.getElementById('c'),ctx=c.getContext('2d'),w=c.width,h=c.height,cnt=500,s=2,stars=[];
Then, for loop:
for(i=0;i<cnt;i++)stars.push({x:Math.random()*400-200,y:Math.random()*300-150,z:Math.random()*100+200});
Using i as the loop variable without let.
But need to make sure variables are properly declared.
Alternatively, using var for all, but in the global scope.
Also, using shorter variable names.
Now, the loop function:
function l(){
ctx.clearRect(0,0,w,h);
for(var i=0;i<cnt;i++){
var s=stars[i];
s.z-=s;
if(s.z<0){
s.x=Math.random()*400-200;
s.y=Math.random()*300-150;
s.z=Math.random()*100+200;
}
var sc=500; // scale
var cx=(s.x/s.z)*sc +w/2,
cy=(s.y/s.z)*sc +h/2,
r=(sc/s.z)*2;
ctx.fillStyle='rgba(255,255,255,'+(r/20)+')';
ctx.beginPath();
ctx.arc(cx,cy,r,0,Math.PI*2);
ctx.fill();
}
ctx.fillText('Stars: '+cnt+' Speed: '+s,10,h-10);
requestAnimationFrame(l);
}
Wait, but in the code above, I used 's' as both the star object and the speed variable. That's a problem. Because in the line s.z -= s; the 's' would refer to the speed variable (speed is named s). Oh right! This is a conflict. The variable 's' here is both the loop variable's star and the variable s which is speed.
So, this is a mistake. So to avoid that, perhaps rename variables.
Original variables:
Speed is 's' (short for speed), so in the loop over stars, each star is 'star' variable (maybe 'st' as a name).
Wait, in the for loop over stars:
for(var i=0;i<cnt;i++){
var st=stars[i]; // st is the star object
st.z -= s; // s is the speed.
if (st.z <0) { reset x,y,z }
... etc.
}
So that way, no variable name clash.
Hence, the code inside the loop:
function l(){
ctx.clearRect(0,0,w,h);
for(var i=0;i<cnt;i++){
var st=stars[i];
st.z-=s;
if(st.z<0){
st.x=Math.random()*400-200;
st.y=Math.random()*300-150;
st.z=Math.random()*100+200;
}
var sc=500,cx=(st.x/sc)? No, wait, the original calculation was:
cx = (st.x / st.z)*scale + w/2,
same for cy.
So:
var sc=500;
cx=(st.x/st.z)*sc +w/2;
cy=(st.y/st.z)*sc +h/2;
r=(sc/st.z)*2;
...
}
Okay, that makes sense. So variables like sc (scale), cx, cy, r can be declared inside the loop.
But wait, inside the loop for each star. So, variables inside the star loop.
So adjusting the variable names to prevent conflicts.
Also, the fillStyle can be written more concisely, perhaps using template strings:
ctx.fillStyle = `rgba(255,255,255,${r/20})`;
But since the problem says no comments, but template literals are okay.
Wait, but the problem says "use only the 2D canvas context — no WebGL, no libraries." We're okay.
Now, putting this all into code as compact as possible.
Also, the initial setup:
At the start, after defining variables, the star array is initialized with:
for(var i=0;i<cnt;i++){
stars.push({
x:Math.random()*400-200,
y:Math.random()*300-150,
z:Math.random()*100+200
});
}
Wait, but in JS, you can write that as:
stars.push({x:Math.random()*400-200,y:Math.random()*300-150,z:Math.random()*100+200});
Now, the complete code in the HTML:
<!DOCTYPE html><html><body><canvas id=c width=640 height=360></canvas><script>
var c=document.getElementById('c'),ctx=c.getContext('2d'),w=c.width,h=c.height,cnt=500,s=2,stars=[];
for(var i=0;i<cnt;i++)stars.push({x:Math.random()*400-200,y:Math.random()*300-150,z:Math.random()*100+200});
function l(){
ctx.clearRect(0,0,w,h);
for(var i=0;i<cnt;i++){
var st=stars[i];
st.z-=s;
if(st.z<0){
st.x=Math.random()*400-200;
st.y=Math.random()*300-150;
st.z=Math.random()*100+200;
}
var sc=500,cx=(st.x/st.z)*sc +w/2,cy=(st.y/st.z)*sc +h/2,r=(sc/st.z)*2;
ctx.fillStyle=`rgba(255,255,255,${r/20})`;
ctx.beginPath();
ctx.arc(cx,cy,r,0,Math.PI*2);
ctx.fill();
}
ctx.fillText(`Stars: ${cnt} Speed: ${s}`,10,h-10);
requestAnimationFrame(l);
}
l();
</script></body></html>
Wait, but in the first line, the canvas is written as <canvas id=c ...> which is valid HTML, since the id is 'c'.
Also, the code starts the animation with l(); but l() is a function that calls requestAnimationFrame(l). So that should work.
Wait, but when you call l(), it does the first frame, then schedules another, so it's okay.
Testing the code, the stars should move towards the viewer (since z decreases each frame by speed s=2), and when z becomes negative, they are reset to positive.
The random x and y when reset are within -200 to +200 (x) and -150 to +150 (y), which assuming the scale of 500, which when divided by z (which is 200-300 at reset), gives:
cx = (x / z)*500 + 320 (since w=640, so 320 center).
So for x=200, z=200: 200/200 *500 =500 → cx=500 + 320? Wait, 200/200 is 1, multiply by 500: 500, plus 320 (w/2 is 320) → 820? But the canvas is 640px wide, so 820 is outside. Hmm, that's an issue.
Ah, perhaps the scaling is wrong.
Wait, let me recalculate:
If the scale is set to 500, then (x / z)*500 is the offset from the center.
If z is 200, then:
x can be up to 200 (max x is 200), so (200/200)*500 = 500 → the cx would be 500 + (w/2)= 320 → 500+320=820, which is beyond the canvas width of 640.
So this would cause the stars to be drawn off-screen.
Which is not desired.
Hence, the scaling is too big. Let's recalculate the scale.
We need to set the scale such that the maximum x/z * scale is less than half the canvas width.
Assume that the maximum x is 200, and minimum z is 200 (since at reset, z is between 200-300). So:
max_x / min_z * scale <= 320 (half of 640).
200/200 * scale = 1 * scale → so scale must be <= 320. So setting the scale to 320.
Similarly, for y: max y is 150, but the canvas height is 360 → half is 180.
So 150/200 * scaleY → scale for y should be 180*(200/150) → but since x and y need to have same scaling.
Alternatively, adjust the scaling to 320 (for x) and then the y scaling can be scaled proportionally.
But since the code uses the same scale for x and y, perhaps we should adjust to the smaller dimension.
Alternatively, let's set sc to 300 instead of 500. Let's see:
If sc=300:
max x/z is 200/200 * 300 = 300 → cx = 300 + 320 → 620 (within canvas width 640). That's okay.
Similarly, for y:
max y is 150, so 150/200 * 300 = 225 → cy = 225 + 180 (h/2) = 405, which is within 360 height? Wait, canvas height is 360, so 180 is center. So 225+180=405 which exceeds 360? 360-180 is 180, so it would be outside.
Hmm, so need to set the scale so that for y:
max_y / min_z * scale <= 180 (half height).
Max y is 150, min z is 200:
150/200 * scaleY <= 180 → scale <= (180 *200)/150 = 240.
So for y, scale must be ≤240.
Therefore, to satisfy both x and y, the scale should be 240.
But then, for x:
max x/z * scale = 200/200 *240=240, so cx=240 +320=560 <640 okay.
cy would be 150/200 *240= 180 → 180+180= 360 (edge), okay.
Hence, setting sc=240.
Thus, in the code, changing sc=240 instead of 500.
So adjusting:
var sc=240;
cx=(st.x/st.z)*sc +w/2,
cy=(st.y/st.z)*sc +h/2,
r = (sc/st.z)*2;
Wait but then the radius would be (240/z)*2. When z is 200, radius is (240/200)*2 = 2.4 → small radius.
But when z is smaller (approaching zero), radius becomes larger.
Hmm, but when z is 200, the star is reset, so maybe the max radius is when z is at its smallest before reset.
Wait, but z decreases each frame by s=2. So starting at z=200, after 100 frames, z would be 0, but in reality it's reset when z becomes negative.
Wait, let me see: when a star's z is 200 (initial value), and each frame decreases by s=2, so after 100 frames, it would reach 0, then be reset. So the time before reset is about 100 frames (at 60fps, ~1.7 seconds).
The radius is (240 / z) * 2. So at z=200, radius is 2.4, at z=100 it's (240/100)*2=4.8, z=50, radius 9.6, etc.
The maximum radius before reset is when z is approaching 0, so radius becomes very large, but since the code resets when z <0, it's okay.
The problem is that when z is small, the star could be large and off-screen?
But with the scale set to 240, and the position calculations, it should stay on screen.
Alternatively, perhaps the scale should be 160 to make sure.
But let's proceed with 240 for now.
So adjusting the code:
var sc=240;
cx = (st.x / st.z)*sc +w/2,
Similarly for y.
Thus, in the code, changing sc=240.
Also, the radius is (sc/z)*2. So with sc=240, at z=200, 240/200=1.2 → *2 is 2.4.
So that's okay.
Another possible issue: the random x and y when resetting are between -200 and +200 (x) and -150 to +150 (y). This is okay because with sc=240, the max x/z (when z is 200) gives 200/200 *240 =240, so the position is 240 + 320 (w/2) → 560, which is within 640.
Similarly, min x is -200, which gives -240 +320 → 80, so okay.
Thus, scaling to 240 is better.
Hence, the corrected code would have sc=240.
So the code:
var sc=240,
cx=(st.x/st.z)*sc +w/2,
cy=(st.y/st.z)*sc +h/2,
r=(sc/st.z)*2;
Also, the fillStyle alpha is r/20, so when radius is 2.4, alpha is 0.12, which is very transparent. Maybe the alpha should be higher.
Alternatively, perhaps the alpha should be independent of radius. Maybe it's better to have a constant alpha, but the user's original code had r/20. Maybe acceptable.
Alternatively, we can try 240 and see.
So let me adjust the code to use sc=240:
Adjusting the code:
<!DOCTYPE html><html><body><canvas id=c width=640 height=360></canvas><script>
var c=document.getElementById('c'),ctx=c.getContext('2d'),w=c.width,h=c.height,cnt=500,s=2,stars=[];
for(var i=0;i<cnt;i++)stars.push({x:Math.random()*400-200,y:Math.random()*300-150,z:Math.random()*100+200});
function l(){
ctx.clearRect(0,0,w,h);
for(var i=0;i<cnt;i++){
var st=stars[i];
st.z -= s;
if(st.z <0){
st.x= Math.random()*400-200;
st.y= Math.random()*300-150;
st.z= Math.random()*100+200;
}
var sc=240,cx=(st.x/st.z)*sc +w/2,cy=(st.y/st.z)*sc +h/2,r=(sc/st.z)*2;
ctx.fillStyle=`rgba(255,255,255,${r/20})`;
ctx.beginPath();
ctx.arc(cx,cy,r,0,Math.PI*2);
ctx.fill();
}
ctx.fillText(`Stars: ${cnt} Speed: ${s}`,10,h-10);
requestAnimationFrame(l);
}
l();
</script></body></html>
Wait, in the initial array initialization, the x was Math.random()*400-200, which gives a range from -200 to +200 (since Math.random()*400 can be 0 to 400, minus 200 → -200 to 200). Similarly for y: Math.random()*300-150 gives -150 to 150. So that's better.
Earlier mistake had 400 instead of 400-200, but in code above, it's correct.
Thus, this should work.
Testing this code should create a parallax effect where stars move from far (z=200-300) towards the viewer (z decreases), and when they reach behind (z<0) they reset to far away.
The radius grows as they get closer, and the alpha (transparency) increases as radius increases (since r/20 when r is larger).
Wait, the alpha is r/20. So when radius is 2.4, alpha is 0.12 (almost transparent), when radius is 24 (when z=10), alpha is 1.2 (over 1, but in rgba it's clamped to 1). So the alpha is fully opaque when r >=20.
So stars that are closer (radius 20 or more) are fully white, and slightly transparent when further away.
This should give a nice parallax effect.
So this should be the corrected code.