Reorganize artifacts by model and harness
This commit is contained in:
1 parent
4027ba94c2
commit
a12017f31e
71 files changed
+95
-48
No files matched your search
@@ -0,0 +1,759 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mythical Minecraft Clone</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background-color: #87CEEB;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#crosshair {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: -10px;
|
||||
margin-left: -10px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
text-shadow: 1px 1px 2px black;
|
||||
}
|
||||
|
||||
#instructions {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(10, 15, 30, 0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
color: white;
|
||||
z-index: 20;
|
||||
transition: opacity 0.4s;
|
||||
}
|
||||
|
||||
#instructions h1 {
|
||||
font-size: 5rem;
|
||||
margin-bottom: 10px;
|
||||
background: linear-gradient(90deg, #00FFFF, #FF00FF);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: glow 2s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
from { text-shadow: 0 0 10px rgba(0,255,255,0.2); }
|
||||
to { text-shadow: 0 0 20px rgba(0,255,255,0.6), 0 0 30px rgba(255,0,255,0.4); }
|
||||
}
|
||||
|
||||
#instructions p {
|
||||
font-size: 1.2rem;
|
||||
color: #AAA;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
padding: 15px 50px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 2px solid #00FFFF;
|
||||
border-radius: 40px;
|
||||
color: white;
|
||||
margin-top: 30px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 0 15px rgba(0,255,255,0.3);
|
||||
}
|
||||
|
||||
.play-btn:hover {
|
||||
background: #00FFFF;
|
||||
color: #0B0B1A;
|
||||
box-shadow: 0 0 30px rgba(0,255,255,0.8);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
#inventory {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
z-index: 10;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(5px);
|
||||
padding: 15px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.slot {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 3px solid #444;
|
||||
background-color: rgba(255,255,255,0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
padding-bottom: 5px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.slot::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background-size: cover;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#slot-1::before { background-color: #557A2B; border-bottom: 4px solid #5A3A22; }
|
||||
#slot-2::before { background-color: #5A3A22; }
|
||||
#slot-3::before { background-color: #7D7D7D; }
|
||||
#slot-4::before { background-color: #4A0080; box-shadow: 0 0 10px #00FFFF; }
|
||||
#slot-5::before { background-color: #FFD700; }
|
||||
|
||||
.slot.active {
|
||||
border-color: #00FFFF;
|
||||
box-shadow: 0 0 20px rgba(0,255,255,0.5);
|
||||
transform: translateY(-10px) scale(1.1);
|
||||
background-color: rgba(0,255,255,0.1);
|
||||
}
|
||||
|
||||
.hotkey {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 5px;
|
||||
font-size: 10px;
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="instructions">
|
||||
<h1>MYTHIC REALMS</h1>
|
||||
<p>A voxel adventure built in a single HTML file.<br>Explore the infinite terrain, find the floating temple, and build your own legend.</p>
|
||||
<p style="color: #00FFFF; margin-top: 10px;">WASD to Move • Mouse to Look • Space to Jump<br>Left Click to Break • Right Click to Place • 1-5 to Select Block</p>
|
||||
<button class="play-btn" id="play-btn">ENTER REALM</button>
|
||||
</div>
|
||||
<div id="crosshair">+</div>
|
||||
<div id="inventory">
|
||||
<div class="slot active" id="slot-1"><span class="hotkey">1</span>Grass</div>
|
||||
<div class="slot" id="slot-2"><span class="hotkey">2</span>Dirt</div>
|
||||
<div class="slot" id="slot-3"><span class="hotkey">3</span>Stone</div>
|
||||
<div class="slot" id="slot-4"><span class="hotkey">4</span>Magic</div>
|
||||
<div class="slot" id="slot-5"><span class="hotkey">5</span>Gold</div>
|
||||
</div>
|
||||
|
||||
<!-- Dependencies -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/PointerLockControls.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script>
|
||||
|
||||
<script>
|
||||
// --- 1. PROCEDURAL TEXTURE GENERATOR ---
|
||||
function createBlockTexture(type) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 16;
|
||||
canvas.height = 16;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
function drawNoise(base, c1, c2, c3) {
|
||||
ctx.fillStyle = base;
|
||||
ctx.fillRect(0, 0, 16, 16);
|
||||
for(let i=0; i<256; i++) {
|
||||
const r = Math.random();
|
||||
if (r < 0.2) ctx.fillStyle = c1;
|
||||
else if (r < 0.4) ctx.fillStyle = c2;
|
||||
else if (r < 0.6) ctx.fillStyle = c3;
|
||||
else continue;
|
||||
ctx.fillRect(Math.floor(Math.random()*16), Math.floor(Math.random()*16), 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'dirt') {
|
||||
drawNoise('#5A3A22', '#4A2A12', '#6A4A32', '#3A1A02');
|
||||
} else if (type === 'grass_top') {
|
||||
drawNoise('#557A2B', '#456A1B', '#658A3B', '#355A0B');
|
||||
} else if (type === 'grass_side') {
|
||||
drawNoise('#5A3A22', '#4A2A12', '#6A4A32', '#3A1A02');
|
||||
ctx.fillStyle = '#557A2B';
|
||||
for(let x=0; x<16; x++) {
|
||||
ctx.fillRect(x, 0, 1, 3 + Math.floor(Math.random()*4));
|
||||
}
|
||||
} else if (type === 'stone') {
|
||||
drawNoise('#7D7D7D', '#6D6D6D', '#8D8D8D', '#5D5D5D');
|
||||
} else if (type === 'magic') {
|
||||
drawNoise('#4A0080', '#6A00A0', '#2A0060', '#8A00C0');
|
||||
ctx.fillStyle = '#00FFFF';
|
||||
for(let i=0; i<15; i++) {
|
||||
ctx.fillRect(Math.floor(Math.random()*16), Math.floor(Math.random()*16), 1, 1);
|
||||
}
|
||||
} else if (type === 'gold') {
|
||||
drawNoise('#FFD700', '#FFA500', '#FFFF00', '#DAA520');
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.fillRect(1, 1, 2, 2);
|
||||
ctx.fillRect(13, 13, 2, 2);
|
||||
}
|
||||
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
tex.magFilter = THREE.NearestFilter;
|
||||
return tex;
|
||||
}
|
||||
|
||||
// --- 2. GAME SETUP & THREE.JS ---
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color('#87CEEB');
|
||||
scene.fog = new THREE.Fog('#87CEEB', 20, 60);
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: false }); // False for retro look
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
document.body.appendChild(renderer.domElement);
|
||||
|
||||
// Lighting
|
||||
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.5);
|
||||
scene.add(hemiLight);
|
||||
|
||||
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
|
||||
dirLight.position.set(50, 100, 50);
|
||||
dirLight.castShadow = true;
|
||||
dirLight.shadow.camera.left = -40;
|
||||
dirLight.shadow.camera.right = 40;
|
||||
dirLight.shadow.camera.top = 40;
|
||||
dirLight.shadow.camera.bottom = -40;
|
||||
dirLight.shadow.mapSize.width = 1024;
|
||||
dirLight.shadow.mapSize.height = 1024;
|
||||
scene.add(dirLight);
|
||||
|
||||
// Materials & Geometries
|
||||
const chunkGeometries = {
|
||||
1: new THREE.BoxGeometry(1, 1, 1),
|
||||
2: new THREE.BoxGeometry(1, 1, 1),
|
||||
3: new THREE.BoxGeometry(1, 1, 1),
|
||||
4: new THREE.BoxGeometry(1, 1, 1),
|
||||
5: new THREE.BoxGeometry(1, 1, 1)
|
||||
};
|
||||
|
||||
const materials = {
|
||||
1: [
|
||||
new THREE.MeshLambertMaterial({ map: createBlockTexture('grass_side') }),
|
||||
new THREE.MeshLambertMaterial({ map: createBlockTexture('grass_side') }),
|
||||
new THREE.MeshLambertMaterial({ map: createBlockTexture('grass_top') }),
|
||||
new THREE.MeshLambertMaterial({ map: createBlockTexture('dirt') }),
|
||||
new THREE.MeshLambertMaterial({ map: createBlockTexture('grass_side') }),
|
||||
new THREE.MeshLambertMaterial({ map: createBlockTexture('grass_side') })
|
||||
],
|
||||
2: new THREE.MeshLambertMaterial({ map: createBlockTexture('dirt') }),
|
||||
3: new THREE.MeshLambertMaterial({ map: createBlockTexture('stone') }),
|
||||
4: new THREE.MeshLambertMaterial({ map: createBlockTexture('magic'), emissive: 0x4A0080, emissiveIntensity: 0.6 }),
|
||||
5: new THREE.MeshStandardMaterial({ map: createBlockTexture('gold'), metalness: 0.6, roughness: 0.3 })
|
||||
};
|
||||
|
||||
// --- 3. WORLD GENERATION ---
|
||||
const simplex = new SimplexNoise();
|
||||
const chunks = new Map();
|
||||
let currentChunkX = -999;
|
||||
let currentChunkZ = -999;
|
||||
const renderDistance = 3;
|
||||
|
||||
function getElevation(wx, wz) {
|
||||
let e = 1 * simplex.noise2D(wx / 100, wz / 100) +
|
||||
0.5 * simplex.noise2D(wx / 50, wz / 50) +
|
||||
0.25 * simplex.noise2D(wx / 25, wz / 25);
|
||||
return Math.floor(Math.pow((e / 1.75) * 0.5 + 0.5, 2) * 30 + 10);
|
||||
}
|
||||
|
||||
function generateMythicalStructure(wx, wy, wz) {
|
||||
const dist = Math.sqrt(wx*wx + wz*wz);
|
||||
|
||||
// 1. Floating Island Base
|
||||
if (dist < 40 && wy >= 45 && wy < 60) {
|
||||
const islandBottom = 60 - Math.pow((40 - dist) / 40, 0.7) * 15;
|
||||
if (wy >= islandBottom) {
|
||||
if (wy === 59) return 1;
|
||||
else if (wy > 55) return 2;
|
||||
else return 3;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Temple Structure
|
||||
if (wy >= 60 && wy < 110) {
|
||||
if (dist < 25 && wy === 60) return 3; // Floor
|
||||
|
||||
// Circular stairs
|
||||
if (dist < 20 && wy === 61) return 3;
|
||||
if (dist < 15 && wy === 62) return 3;
|
||||
if (dist < 10 && wy === 63) return 3;
|
||||
|
||||
// Pillars
|
||||
const angle = Math.atan2(wz, wx);
|
||||
const numPillars = 8;
|
||||
const segment = (Math.PI * 2) / numPillars;
|
||||
const nearestPillarAngle = Math.round(angle / segment) * segment;
|
||||
const px = Math.cos(nearestPillarAngle) * 18;
|
||||
const pz = Math.sin(nearestPillarAngle) * 18;
|
||||
|
||||
if (Math.abs(wx - px) <= 1 && Math.abs(wz - pz) <= 1 && wy >= 60 && wy <= 80) {
|
||||
return (wy % 6 === 0) ? 5 : 3;
|
||||
}
|
||||
|
||||
// Roof
|
||||
if (wy >= 81 && wy < 95) {
|
||||
const roofRadius = 22 - (wy - 81) * 1.5;
|
||||
if (dist < roofRadius && dist > roofRadius - 2) {
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
// Central Altar
|
||||
if (dist < 3 && wy >= 64 && wy <= 65) return 5;
|
||||
if (dist < 1 && wy === 66) return 4;
|
||||
|
||||
// Floating magic halo
|
||||
if (wy === 100 && Math.abs(dist - 10) < 1) return 5;
|
||||
}
|
||||
|
||||
// 3. Floating crystals
|
||||
const hash = Math.abs(Math.sin(wx * 12.9898 + wz * 78.233) * 43758.5453);
|
||||
if (hash > 0.999 && dist > 45 && dist < 100 && wy > 50 && wy < 90) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
class Chunk {
|
||||
constructor(cx, cz) {
|
||||
this.cx = cx;
|
||||
this.cz = cz;
|
||||
this.blocks = new Uint8Array(16 * 128 * 16);
|
||||
this.meshes = {};
|
||||
this.generate();
|
||||
this.updateMesh();
|
||||
}
|
||||
|
||||
generate() {
|
||||
for(let x=0; x<16; x++) {
|
||||
for(let z=0; z<16; z++) {
|
||||
const wx = this.cx * 16 + x;
|
||||
const wz = this.cz * 16 + z;
|
||||
const elevation = getElevation(wx, wz);
|
||||
const distToCenter = Math.sqrt(wx*wx + wz*wz);
|
||||
|
||||
for(let y=0; y<128; y++) {
|
||||
let type = 0;
|
||||
const mythicType = generateMythicalStructure(wx, y, wz);
|
||||
|
||||
if (mythicType !== null) {
|
||||
type = mythicType;
|
||||
} else if (y <= elevation) {
|
||||
// Clear a space beneath the floating island
|
||||
if (distToCenter < 45 && y > 30) {
|
||||
type = 0;
|
||||
} else {
|
||||
if (y === elevation) type = 1;
|
||||
else if (y > elevation - 4) type = 2;
|
||||
else type = 3;
|
||||
}
|
||||
}
|
||||
this.blocks[x + z*16 + y*256] = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isSolid(bx, y, bz) {
|
||||
if (bx < 0 || bx >= 16 || bz < 0 || bz >= 16 || y < 0 || y >= 128) {
|
||||
return getBlock(this.cx * 16 + bx, y, this.cz * 16 + bz) !== 0;
|
||||
}
|
||||
return this.blocks[bx + bz * 16 + y * 256] !== 0;
|
||||
}
|
||||
|
||||
updateMesh() {
|
||||
for (let type in this.meshes) {
|
||||
scene.remove(this.meshes[type]);
|
||||
this.meshes[type].dispose();
|
||||
}
|
||||
this.meshes = {};
|
||||
|
||||
const counts = {};
|
||||
for(let i=0; i<this.blocks.length; i++) {
|
||||
const type = this.blocks[i];
|
||||
if (type !== 0) {
|
||||
const x = i % 16;
|
||||
const z = Math.floor(i / 16) % 16;
|
||||
const y = Math.floor(i / 256);
|
||||
|
||||
if (this.isSolid(x+1, y, z) && this.isSolid(x-1, y, z) &&
|
||||
this.isSolid(x, y+1, z) && this.isSolid(x, y-1, z) &&
|
||||
this.isSolid(x, y, z+1) && this.isSolid(x, y, z-1)) {
|
||||
continue;
|
||||
}
|
||||
counts[type] = (counts[type] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (let type in counts) {
|
||||
if (counts[type] > 0) {
|
||||
this.meshes[type] = new THREE.InstancedMesh(chunkGeometries[type], materials[type], counts[type]);
|
||||
this.meshes[type].frustumCulled = false;
|
||||
|
||||
// Only let some chunks cast shadows to save performance
|
||||
if (Math.abs(this.cx) <= 1 && Math.abs(this.cz) <= 1) {
|
||||
this.meshes[type].castShadow = true;
|
||||
this.meshes[type].receiveShadow = true;
|
||||
}
|
||||
scene.add(this.meshes[type]);
|
||||
}
|
||||
}
|
||||
|
||||
const dummy = new THREE.Object3D();
|
||||
const counters = {};
|
||||
for (let type in counts) counters[type] = 0;
|
||||
|
||||
for(let i=0; i<this.blocks.length; i++) {
|
||||
const type = this.blocks[i];
|
||||
if (type !== 0) {
|
||||
const x = i % 16;
|
||||
const z = Math.floor(i / 16) % 16;
|
||||
const y = Math.floor(i / 256);
|
||||
|
||||
if (this.isSolid(x+1, y, z) && this.isSolid(x-1, y, z) &&
|
||||
this.isSolid(x, y+1, z) && this.isSolid(x, y-1, z) &&
|
||||
this.isSolid(x, y, z+1) && this.isSolid(x, y, z-1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.meshes[type]) {
|
||||
dummy.position.set(this.cx*16 + x, y, this.cz*16 + z);
|
||||
dummy.updateMatrix();
|
||||
this.meshes[type].setMatrixAt(counters[type]++, dummy.matrix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let type in this.meshes) {
|
||||
this.meshes[type].instanceMatrix.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getBlock(x, y, z) {
|
||||
const cx = Math.floor(x / 16);
|
||||
const cz = Math.floor(z / 16);
|
||||
const chunk = chunks.get(`${cx},${cz}`);
|
||||
if (!chunk || y < 0 || y >= 128) return 0;
|
||||
const bx = x - cx * 16;
|
||||
const bz = z - cz * 16;
|
||||
return chunk.blocks[bx + bz * 16 + y * 256];
|
||||
}
|
||||
|
||||
function setBlock(x, y, z, type) {
|
||||
const cx = Math.floor(x / 16);
|
||||
const cz = Math.floor(z / 16);
|
||||
const chunk = chunks.get(`${cx},${cz}`);
|
||||
if (!chunk || y < 0 || y >= 128) return;
|
||||
|
||||
const bx = x - cx * 16;
|
||||
const bz = z - cz * 16;
|
||||
chunk.blocks[bx + bz * 16 + y * 256] = type;
|
||||
chunk.updateMesh();
|
||||
|
||||
// Update neighbor chunks if on edge
|
||||
if (bx === 0) chunks.get(`${cx-1},${cz}`)?.updateMesh();
|
||||
if (bx === 15) chunks.get(`${cx+1},${cz}`)?.updateMesh();
|
||||
if (bz === 0) chunks.get(`${cx},${cz-1}`)?.updateMesh();
|
||||
if (bz === 15) chunks.get(`${cx},${cz+1}`)?.updateMesh();
|
||||
}
|
||||
|
||||
function updateChunks(px, pz) {
|
||||
const cx = Math.floor(Math.round(px) / 16);
|
||||
const cz = Math.floor(Math.round(pz) / 16);
|
||||
|
||||
if (cx !== currentChunkX || cz !== currentChunkZ || chunks.size === 0) {
|
||||
currentChunkX = cx;
|
||||
currentChunkZ = cz;
|
||||
|
||||
for(let x = cx - renderDistance; x <= cx + renderDistance; x++) {
|
||||
for(let z = cz - renderDistance; z <= cz + renderDistance; z++) {
|
||||
const key = `${x},${z}`;
|
||||
if (!chunks.has(key)) {
|
||||
chunks.set(key, new Chunk(x, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, chunk] of chunks.entries()) {
|
||||
if (Math.abs(chunk.cx - cx) > renderDistance + 1 ||
|
||||
Math.abs(chunk.cz - cz) > renderDistance + 1) {
|
||||
for (let type in chunk.meshes) {
|
||||
scene.remove(chunk.meshes[type]);
|
||||
chunk.meshes[type].dispose();
|
||||
}
|
||||
chunks.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 4. PLAYER & PHYSICS ---
|
||||
const player = {
|
||||
position: new THREE.Vector3(0, 70, 20), // Spawn looking at the temple
|
||||
velocity: new THREE.Vector3(0, 0, 0),
|
||||
onGround: false,
|
||||
speed: 8,
|
||||
jumpForce: 10,
|
||||
gravity: 25
|
||||
};
|
||||
camera.position.copy(player.position);
|
||||
|
||||
const controls = new THREE.PointerLockControls(camera, document.body);
|
||||
const instructions = document.getElementById('instructions');
|
||||
const playBtn = document.getElementById('play-btn');
|
||||
|
||||
playBtn.addEventListener('click', () => controls.lock());
|
||||
controls.addEventListener('lock', () => instructions.style.opacity = '0');
|
||||
controls.addEventListener('unlock', () => instructions.style.opacity = '1');
|
||||
// Actually hide it after transition
|
||||
instructions.addEventListener('transitionend', (e) => {
|
||||
if (instructions.style.opacity === '0') instructions.style.display = 'none';
|
||||
});
|
||||
// Show immediately before transition
|
||||
controls.addEventListener('unlock', () => {
|
||||
instructions.style.display = 'flex';
|
||||
setTimeout(() => instructions.style.opacity = '1', 10);
|
||||
});
|
||||
|
||||
const keys = {};
|
||||
document.addEventListener('keydown', e => keys[e.code] = true);
|
||||
document.addEventListener('keyup', e => keys[e.code] = false);
|
||||
|
||||
function checkCollision(pos) {
|
||||
const pr = 0.3;
|
||||
const pMinX = pos.x - pr;
|
||||
const pMaxX = pos.x + pr;
|
||||
const pMinY = pos.y;
|
||||
const pMaxY = pos.y + 1.6; // Player height
|
||||
const pMinZ = pos.z - pr;
|
||||
const pMaxZ = pos.z + pr;
|
||||
|
||||
const bMinX = Math.floor(pMinX + 0.5);
|
||||
const bMaxX = Math.floor(pMaxX + 0.5);
|
||||
const bMinY = Math.floor(pMinY + 0.5);
|
||||
const bMaxY = Math.floor(pMaxY + 0.5);
|
||||
const bMinZ = Math.floor(pMinZ + 0.5);
|
||||
const bMaxZ = Math.floor(pMaxZ + 0.5);
|
||||
|
||||
for (let x = bMinX; x <= bMaxX; x++) {
|
||||
for (let y = bMinY; y <= bMaxY; y++) {
|
||||
for (let z = bMinZ; z <= bMaxZ; z++) {
|
||||
if (getBlock(x, y, z) !== 0) {
|
||||
const blockMinX = x - 0.5;
|
||||
const blockMaxX = x + 0.5;
|
||||
const blockMinY = y - 0.5;
|
||||
const blockMaxY = y + 0.5;
|
||||
const blockMinZ = z - 0.5;
|
||||
const blockMaxZ = z + 0.5;
|
||||
|
||||
if (pMinX < blockMaxX && pMaxX > blockMinX &&
|
||||
pMinY < blockMaxY && pMaxY > blockMinY &&
|
||||
pMinZ < blockMaxZ && pMaxZ > blockMinZ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function updatePlayer(delta) {
|
||||
player.velocity.y -= player.gravity * delta;
|
||||
|
||||
const moveDir = new THREE.Vector3();
|
||||
const forward = new THREE.Vector3();
|
||||
camera.getWorldDirection(forward);
|
||||
forward.y = 0;
|
||||
forward.normalize();
|
||||
|
||||
const right = new THREE.Vector3();
|
||||
right.crossVectors(forward, new THREE.Vector3(0, 1, 0)).normalize();
|
||||
|
||||
if (keys['KeyW']) moveDir.add(forward);
|
||||
if (keys['KeyS']) moveDir.sub(forward);
|
||||
if (keys['KeyD']) moveDir.add(right);
|
||||
if (keys['KeyA']) moveDir.sub(right);
|
||||
|
||||
if (moveDir.lengthSq() > 0) moveDir.normalize();
|
||||
|
||||
const moveStep = moveDir.multiplyScalar(player.speed * delta);
|
||||
|
||||
player.position.x += moveStep.x;
|
||||
if (checkCollision(player.position)) player.position.x -= moveStep.x;
|
||||
|
||||
player.position.z += moveStep.z;
|
||||
if (checkCollision(player.position)) player.position.z -= moveStep.z;
|
||||
|
||||
player.position.y += player.velocity.y * delta;
|
||||
player.onGround = false;
|
||||
|
||||
if (checkCollision(player.position)) {
|
||||
if (player.velocity.y < 0) {
|
||||
player.onGround = true;
|
||||
player.position.y = Math.floor(player.position.y) + 0.5;
|
||||
} else {
|
||||
player.position.y = Math.floor(player.position.y + 1.6) - 1.6 + 0.5;
|
||||
}
|
||||
player.velocity.y = 0;
|
||||
}
|
||||
|
||||
if (keys['Space'] && player.onGround) {
|
||||
player.velocity.y = player.jumpForce;
|
||||
}
|
||||
|
||||
if (player.position.y < -20) {
|
||||
player.position.set(0, 80, 20); // Reset if fall off
|
||||
player.velocity.y = 0;
|
||||
}
|
||||
|
||||
camera.position.copy(player.position);
|
||||
camera.position.y += 1.4; // Eye height relative to feet
|
||||
|
||||
// Move light to follow player for dynamic shadows
|
||||
dirLight.position.set(player.position.x + 50, player.position.y + 100, player.position.z + 50);
|
||||
dirLight.target.position.copy(player.position);
|
||||
dirLight.target.updateMatrixWorld();
|
||||
}
|
||||
|
||||
// --- 5. INTERACTION & INVENTORY ---
|
||||
let selectedBlock = 1;
|
||||
|
||||
function setSelectedBlock(type) {
|
||||
selectedBlock = type;
|
||||
document.querySelectorAll('.slot').forEach(el => el.classList.remove('active'));
|
||||
document.getElementById(`slot-${type}`).classList.add('active');
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.code === 'Digit1') setSelectedBlock(1);
|
||||
if (e.code === 'Digit2') setSelectedBlock(2);
|
||||
if (e.code === 'Digit3') setSelectedBlock(3);
|
||||
if (e.code === 'Digit4') setSelectedBlock(4);
|
||||
if (e.code === 'Digit5') setSelectedBlock(5);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.slot').forEach(el => {
|
||||
el.addEventListener('click', (e) => {
|
||||
const type = parseInt(e.currentTarget.id.split('-')[1]);
|
||||
setSelectedBlock(type);
|
||||
});
|
||||
});
|
||||
|
||||
function raycastVoxel(origin, direction, maxDistance) {
|
||||
let t = 0;
|
||||
const step = 0.05;
|
||||
const pos = new THREE.Vector3();
|
||||
let lastBlockPos = null;
|
||||
|
||||
while (t < maxDistance) {
|
||||
pos.copy(origin).addScaledVector(direction, t);
|
||||
const bx = Math.floor(pos.x + 0.5);
|
||||
const by = Math.floor(pos.y + 0.5);
|
||||
const bz = Math.floor(pos.z + 0.5);
|
||||
|
||||
const type = getBlock(bx, by, bz);
|
||||
if (type !== 0) {
|
||||
return { hit: new THREE.Vector3(bx, by, bz), normalPos: lastBlockPos };
|
||||
}
|
||||
|
||||
lastBlockPos = new THREE.Vector3(bx, by, bz);
|
||||
t += step;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', (e) => {
|
||||
if (!controls.isLocked) return;
|
||||
|
||||
const dir = new THREE.Vector3();
|
||||
camera.getWorldDirection(dir);
|
||||
const hitInfo = raycastVoxel(camera.position, dir, 8);
|
||||
|
||||
if (hitInfo) {
|
||||
if (e.button === 0) {
|
||||
setBlock(hitInfo.hit.x, hitInfo.hit.y, hitInfo.hit.z, 0);
|
||||
} else if (e.button === 2) {
|
||||
if (hitInfo.normalPos) {
|
||||
const p = hitInfo.normalPos;
|
||||
const oldType = getBlock(p.x, p.y, p.z);
|
||||
setBlock(p.x, p.y, p.z, selectedBlock);
|
||||
if (checkCollision(player.position)) {
|
||||
setBlock(p.x, p.y, p.z, oldType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('contextmenu', e => e.preventDefault());
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
});
|
||||
|
||||
// --- 6. MAIN LOOP ---
|
||||
let lastTime = performance.now();
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
const time = performance.now();
|
||||
const delta = Math.min((time - lastTime) / 1000, 0.1);
|
||||
lastTime = time;
|
||||
|
||||
if (controls.isLocked) {
|
||||
updatePlayer(delta);
|
||||
updateChunks(player.position.x, player.position.z);
|
||||
}
|
||||
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
// Initial chunk generation around spawn
|
||||
updateChunks(player.position.x, player.position.z);
|
||||
animate();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,349 @@
|
||||
<svg viewBox="0 0 1000 700" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<!-- 环境光影渐变 -->
|
||||
<linearGradient id="skyGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#4a90e2" />
|
||||
<stop offset="40%" stop-color="#87ceeb" />
|
||||
<stop offset="80%" stop-color="#ffdfa0" />
|
||||
<stop offset="100%" stop-color="#ffb347" />
|
||||
</linearGradient>
|
||||
|
||||
<radialGradient id="sunGrad" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#ffffff" />
|
||||
<stop offset="20%" stop-color="#ffeeaa" />
|
||||
<stop offset="100%" stop-color="#ffb347" stop-opacity="0" />
|
||||
</radialGradient>
|
||||
|
||||
<linearGradient id="seaGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#005c8a" />
|
||||
<stop offset="40%" stop-color="#0083b0" />
|
||||
<stop offset="100%" stop-color="#00b4db" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="sandGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#e6d3a8" />
|
||||
<stop offset="100%" stop-color="#c2a77a" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 自行车材质渐变 -->
|
||||
<linearGradient id="frameGrad" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#00e5ff" />
|
||||
<stop offset="50%" stop-color="#00b4db" />
|
||||
<stop offset="100%" stop-color="#0083b0" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="chromeGrad" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#ffffff" />
|
||||
<stop offset="50%" stop-color="#cccccc" />
|
||||
<stop offset="100%" stop-color="#999999" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="tireGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#444444" />
|
||||
<stop offset="100%" stop-color="#111111" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 鹈鹕材质渐变 -->
|
||||
<radialGradient id="bodyGrad" cx="50%" cy="50%" r="50%" fx="70%" fy="30%">
|
||||
<stop offset="0%" stop-color="#ffffff" />
|
||||
<stop offset="70%" stop-color="#e6ecef" />
|
||||
<stop offset="100%" stop-color="#a0b4c0" />
|
||||
</radialGradient>
|
||||
|
||||
<linearGradient id="beakTopGrad" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#ffe066" />
|
||||
<stop offset="60%" stop-color="#ffb300" />
|
||||
<stop offset="100%" stop-color="#f57c00" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="beakPouchGrad" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#ffcc80" />
|
||||
<stop offset="40%" stop-color="#ff8a65" />
|
||||
<stop offset="80%" stop-color="#e64a19" />
|
||||
<stop offset="100%" stop-color="#bf360c" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 滤镜 -->
|
||||
<filter id="blur-shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="8" />
|
||||
</filter>
|
||||
|
||||
<filter id="blur-cloud" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="6" />
|
||||
</filter>
|
||||
|
||||
<!-- 复用组件:车轮 -->
|
||||
<g id="wheel">
|
||||
<!-- 外胎 -->
|
||||
<circle cx="0" cy="0" r="90" fill="none" stroke="url(#tireGrad)" stroke-width="14" />
|
||||
<!-- 白边复古轮胎圈 -->
|
||||
<circle cx="0" cy="0" r="82" fill="none" stroke="#f4f4f4" stroke-width="5" />
|
||||
<!-- 金属轮毂 -->
|
||||
<circle cx="0" cy="0" r="77" fill="none" stroke="url(#chromeGrad)" stroke-width="6" />
|
||||
<!-- 交叉辐条 -->
|
||||
<g stroke="#cfcfcf" stroke-width="1.5">
|
||||
<line x1="-75" y1="0" x2="75" y2="0" />
|
||||
<line x1="0" y1="-75" x2="0" y2="75" />
|
||||
<line x1="-53" y1="-53" x2="53" y2="53" />
|
||||
<line x1="-53" y1="53" x2="53" y2="-53" />
|
||||
<line x1="-69" y1="-29" x2="69" y2="29" />
|
||||
<line x1="-69" y1="29" x2="69" y2="-29" />
|
||||
<line x1="-29" y1="-69" x2="29" y2="69" />
|
||||
<line x1="-29" y1="69" x2="29" y2="-69" />
|
||||
</g>
|
||||
<!-- 轴心 -->
|
||||
<circle cx="0" cy="0" r="8" fill="url(#chromeGrad)" stroke="#666" stroke-width="2"/>
|
||||
</g>
|
||||
</defs>
|
||||
|
||||
<!-- 背景:天空与太阳 -->
|
||||
<rect x="0" y="0" width="1000" height="400" fill="url(#skyGrad)" />
|
||||
<!-- 太阳光晕 -->
|
||||
<circle cx="800" cy="220" r="150" fill="url(#sunGrad)" />
|
||||
<circle cx="800" cy="220" r="40" fill="#ffffff" filter="url(#blur-cloud)" />
|
||||
|
||||
<!-- 远处的飞鸟 -->
|
||||
<g fill="#ffffff" opacity="0.6">
|
||||
<path d="M 150,150 Q 160,140 170,150 Q 180,140 190,150 Q 180,155 170,145 Q 160,155 150,150 Z" />
|
||||
<path d="M 220,120 Q 225,115 230,120 Q 235,115 240,120 Q 235,123 230,118 Q 225,123 220,120 Z" />
|
||||
</g>
|
||||
|
||||
<!-- 云彩 -->
|
||||
<g fill="#ffffff" opacity="0.85" filter="url(#blur-cloud)">
|
||||
<circle cx="200" cy="180" r="30" />
|
||||
<circle cx="240" cy="160" r="45" />
|
||||
<circle cx="290" cy="180" r="35" />
|
||||
<rect x="190" y="170" width="110" height="40" rx="20" />
|
||||
|
||||
<circle cx="850" cy="120" r="25" />
|
||||
<circle cx="890" cy="110" r="35" />
|
||||
<circle cx="930" cy="130" r="20" />
|
||||
</g>
|
||||
|
||||
<!-- 背景:海面 -->
|
||||
<rect x="0" y="400" width="1000" height="120" fill="url(#seaGrad)" />
|
||||
<!-- 水波纹与太阳倒影 -->
|
||||
<g stroke="#ffffff" stroke-width="2" fill="none" opacity="0.4" stroke-linecap="round">
|
||||
<path d="M 150,430 Q 180,430 210,430" />
|
||||
<path d="M 350,460 Q 380,460 410,460" />
|
||||
<path d="M 100,480 Q 140,480 180,480" />
|
||||
<!-- 倒影区域 -->
|
||||
<g stroke="#ffcc80" stroke-width="3" opacity="0.8">
|
||||
<path d="M 750,415 L 850,415" />
|
||||
<path d="M 770,435 L 830,435" />
|
||||
<path d="M 740,455 L 860,455" />
|
||||
<path d="M 780,475 L 820,475" />
|
||||
<path d="M 760,495 L 840,495" />
|
||||
</g>
|
||||
<path d="M 680,440 Q 700,440 720,440" />
|
||||
<path d="M 880,460 Q 900,460 920,460" />
|
||||
</g>
|
||||
|
||||
<!-- 背景:沙滩 -->
|
||||
<path d="M 0,520 Q 250,510 500,520 T 1000,520 L 1000,700 L 0,700 Z" fill="url(#sandGrad)" />
|
||||
<path d="M 0,560 Q 300,530 600,580 T 1000,550 L 1000,700 L 0,700 Z" fill="#b99e71" opacity="0.3" />
|
||||
|
||||
<!-- 投影 -->
|
||||
<path d="M 180,630 L 700,610 L 780,630 L 250,650 Z" fill="rgba(0,0,0,0.25)" filter="url(#blur-shadow)" />
|
||||
<ellipse cx="280" cy="625" rx="50" ry="10" fill="rgba(0,0,0,0.3)" filter="url(#blur-shadow)" />
|
||||
<ellipse cx="720" cy="625" rx="50" ry="10" fill="rgba(0,0,0,0.3)" filter="url(#blur-shadow)" />
|
||||
|
||||
<!-- 左侧(背景层)踏板与鹈鹕左腿 -->
|
||||
<g id="left-leg-and-pedal">
|
||||
<!-- 左侧曲柄 -->
|
||||
<line x1="500" y1="540" x2="475" y2="515" stroke="#888888" stroke-width="7" stroke-linecap="round" />
|
||||
<!-- 左腿大腿 -->
|
||||
<path d="M 370,330 C 380,360 400,390 420,390 C 410,360 390,340 370,330 Z" fill="#a0b4c0" />
|
||||
<!-- 左腿小腿 -->
|
||||
<line x1="415" y1="385" x2="475" y2="515" stroke="#cc7700" stroke-width="8" stroke-linecap="round" />
|
||||
<!-- 左侧脚蹼 -->
|
||||
<path d="M 475,515 L 495,515 L 505,530 L 485,525 Z" fill="#cc7700" />
|
||||
<!-- 左踏板 -->
|
||||
<rect x="463" y="511" width="24" height="8" rx="2" fill="#555555" />
|
||||
</g>
|
||||
|
||||
<!-- 挡泥板(后层) -->
|
||||
<path d="M 190,550 A 100 100 0 0 1 350,450" fill="none" stroke="url(#frameGrad)" stroke-width="12" stroke-linecap="round" />
|
||||
|
||||
<!-- 车轮 -->
|
||||
<use href="#wheel" x="280" y="520" />
|
||||
<use href="#wheel" x="720" y="520" />
|
||||
|
||||
<!-- 自行车车架 -->
|
||||
<g id="bicycle-frame">
|
||||
<!-- 后下叉 (Chainstay) -->
|
||||
<line x1="280" y1="520" x2="500" y2="540" stroke="url(#frameGrad)" stroke-width="12" stroke-linecap="round" />
|
||||
<!-- 后上叉 (Seatstay) -->
|
||||
<line x1="280" y1="520" x2="420" y2="360" stroke="url(#frameGrad)" stroke-width="10" stroke-linecap="round" />
|
||||
<!-- 下管 (Down tube) -->
|
||||
<line x1="500" y1="540" x2="680" y2="400" stroke="url(#frameGrad)" stroke-width="15" stroke-linecap="round" />
|
||||
<!-- 上管 (Top tube) -->
|
||||
<line x1="420" y1="360" x2="660" y2="340" stroke="url(#frameGrad)" stroke-width="14" stroke-linecap="round" />
|
||||
<!-- 立管 (Seat tube) -->
|
||||
<line x1="500" y1="540" x2="420" y2="360" stroke="url(#frameGrad)" stroke-width="14" stroke-linecap="round" />
|
||||
<!-- 头管 (Head tube) -->
|
||||
<line x1="660" y1="340" x2="680" y2="400" stroke="url(#frameGrad)" stroke-width="18" stroke-linecap="round" />
|
||||
<!-- 前叉 (Fork) -->
|
||||
<line x1="680" y1="400" x2="720" y2="520" stroke="url(#chromeGrad)" stroke-width="12" stroke-linecap="round" />
|
||||
<!-- 挡泥板(前层) -->
|
||||
<path d="M 630,550 A 100 100 0 0 1 790,450" fill="none" stroke="url(#frameGrad)" stroke-width="12" stroke-linecap="round" />
|
||||
</g>
|
||||
|
||||
<!-- 座管与座椅 -->
|
||||
<line x1="420" y1="360" x2="410" y2="320" stroke="url(#chromeGrad)" stroke-width="8" />
|
||||
<path d="M 380,320 C 370,310 440,310 450,330 L 380,330 Z" fill="#2a2a2a" />
|
||||
<!-- 座椅弹簧细节 -->
|
||||
<path d="M 395,330 L 390,345 M 435,330 L 430,345" stroke="#999" stroke-width="3" />
|
||||
|
||||
<!-- 传动系统:牙盘与链条 -->
|
||||
<g id="drivetrain">
|
||||
<!-- 链条 -->
|
||||
<path d="M 280,510 L 500,518 M 280,530 L 500,562" stroke="#444444" stroke-width="4" stroke-dasharray="5 3" />
|
||||
<circle cx="280" cy="520" r="10" fill="#999" stroke="#555" stroke-width="2" />
|
||||
<!-- 牙盘 -->
|
||||
<circle cx="500" cy="540" r="24" fill="none" stroke="#555" stroke-width="4" stroke-dasharray="4 2" />
|
||||
<circle cx="500" cy="540" r="18" fill="url(#chromeGrad)" />
|
||||
<line x1="482" y1="540" x2="518" y2="540" stroke="#777" stroke-width="4" />
|
||||
<line x1="500" y1="522" x2="500" y2="558" stroke="#777" stroke-width="4" />
|
||||
</g>
|
||||
|
||||
<!-- 右侧曲柄与踏板 (前景) -->
|
||||
<g id="right-crank">
|
||||
<line x1="500" y1="540" x2="525" y2="565" stroke="url(#chromeGrad)" stroke-width="7" stroke-linecap="round" />
|
||||
<rect x="513" y="561" width="24" height="8" rx="2" fill="#222222" />
|
||||
</g>
|
||||
|
||||
<!-- 鹈鹕主体 (尾巴、身体、脖子、头部) -->
|
||||
<g id="pelican">
|
||||
<!-- 尾羽 -->
|
||||
<path d="M 260,330 L 150,360 L 140,340 L 240,310 Z" fill="#333333" />
|
||||
<path d="M 250,350 L 140,380 L 130,360 L 220,330 Z" fill="#1a1a1a" />
|
||||
|
||||
<!-- 身体 -->
|
||||
<path d="M 380,360
|
||||
C 280,380 200,340 180,280
|
||||
C 160,220 280,200 350,230
|
||||
C 410,250 460,290 450,340
|
||||
C 445,355 420,360 380,360 Z"
|
||||
fill="url(#bodyGrad)" />
|
||||
|
||||
<!-- 脖子 -->
|
||||
<path d="M 350,240
|
||||
C 340,160 430,150 460,110
|
||||
C 470,90 500,80 520,90
|
||||
C 530,110 500,130 480,130
|
||||
C 440,160 410,210 400,260 Z"
|
||||
fill="url(#bodyGrad)" />
|
||||
|
||||
<!-- 喉囊 (下颌) -->
|
||||
<!-- 增加半透明度和阴影表现饱满感 -->
|
||||
<path d="M 505,105 C 570,180 720,210 800,130 C 680,120 580,115 505,105 Z" fill="url(#beakPouchGrad)" />
|
||||
<path d="M 505,105 C 550,150 630,170 730,125" fill="none" stroke="#d84315" stroke-width="2" opacity="0.4" />
|
||||
|
||||
<!-- 上喙 (上颌) -->
|
||||
<path d="M 515,85 Q 680,80 800,120 Q 815,125 808,135 Q 803,135 800,130 Q 680,110 505,105 Z" fill="url(#beakTopGrad)" />
|
||||
|
||||
<!-- 眼睛与眼斑 -->
|
||||
<path d="M 495,90 C 495,80 515,80 515,90 C 515,100 495,100 495,90 Z" fill="#ffe082" stroke="#ffb300" stroke-width="1.5" />
|
||||
<circle cx="505" cy="90" r="5" fill="#111111" />
|
||||
<circle cx="507" cy="88" r="1.5" fill="#ffffff" />
|
||||
</g>
|
||||
|
||||
<!-- 配饰:车篓与捕到的鱼 -->
|
||||
<g id="basket-and-fish">
|
||||
<!-- 车把立管 -->
|
||||
<line x1="660" y1="340" x2="650" y2="290" stroke="url(#chromeGrad)" stroke-width="10" />
|
||||
|
||||
<!-- 车篓底色与轮廓 -->
|
||||
<path d="M 670,290 L 730,290 L 715,360 L 665,360 Z" fill="#d4a373" stroke="#8b5a2b" stroke-width="3" stroke-linejoin="round" />
|
||||
|
||||
<!-- 鱼 -->
|
||||
<g id="fish">
|
||||
<path d="M 685,300 L 665,320 L 685,325 Z" fill="#90a4ae" />
|
||||
<path d="M 685,300 C 680,270 690,250 710,240 C 725,230 745,240 745,240 C 745,240 735,260 715,270 C 700,280 685,300 685,300 Z" fill="url(#chromeGrad)" stroke="#78909c" stroke-width="1" />
|
||||
<circle cx="730" cy="245" r="2.5" fill="#111" />
|
||||
<circle cx="731" cy="244" r="0.8" fill="#fff" />
|
||||
<path d="M 718,245 Q 713,255 718,265" fill="none" stroke="#546e7a" stroke-width="1.5" />
|
||||
</g>
|
||||
|
||||
<!-- 车篓藤条纹理 -->
|
||||
<line x1="675" y1="305" x2="725" y2="305" stroke="#b5835a" stroke-width="4" />
|
||||
<line x1="673" y1="322" x2="721" y2="322" stroke="#b5835a" stroke-width="4" />
|
||||
<line x1="670" y1="340" x2="717" y2="340" stroke="#b5835a" stroke-width="4" />
|
||||
<line x1="685" y1="290" x2="680" y2="360" stroke="#b5835a" stroke-width="4" />
|
||||
<line x1="700" y1="290" x2="695" y2="360" stroke="#b5835a" stroke-width="4" />
|
||||
<line x1="715" y1="290" x2="710" y2="360" stroke="#b5835a" stroke-width="4" />
|
||||
</g>
|
||||
|
||||
<!-- 车把与握把 -->
|
||||
<path d="M 600,270 C 630,260 660,260 680,290" fill="none" stroke="url(#chromeGrad)" stroke-width="10" stroke-linecap="round" />
|
||||
<line x1="585" y1="275" x2="615" y2="265" stroke="#8b4513" stroke-width="16" stroke-linecap="round" />
|
||||
|
||||
<!-- 鹈鹕生动的配饰:红底白点波浪头巾 -->
|
||||
<g id="bandana">
|
||||
<path d="M 400,260 C 410,275 435,275 440,250 L 445,265 C 420,285 395,280 400,260 Z" fill="#d32f2f" />
|
||||
<!-- 飘扬的头巾尾巴 -->
|
||||
<path d="M 408,265 Q 360,280 320,265 Q 360,295 415,275 Z" fill="#b71c1c" />
|
||||
<path d="M 418,268 Q 370,305 340,315 Q 380,325 425,278 Z" fill="#d32f2f" />
|
||||
<!-- 波点 -->
|
||||
<circle cx="410" cy="263" r="2" fill="#ffffff" />
|
||||
<circle cx="425" cy="265" r="2" fill="#ffffff" />
|
||||
<circle cx="435" cy="255" r="2" fill="#ffffff" />
|
||||
<circle cx="390" cy="275" r="1.5" fill="#ffffff" />
|
||||
<circle cx="360" cy="273" r="1.5" fill="#ffffff" />
|
||||
<circle cx="380" cy="295" r="1.5" fill="#ffffff" />
|
||||
<circle cx="350" cy="305" r="1.5" fill="#ffffff" />
|
||||
</g>
|
||||
|
||||
<!-- 鹈鹕右腿 (前景层) -->
|
||||
<g id="right-leg">
|
||||
<path d="M 390,340 C 410,380 440,430 470,440 C 460,400 420,360 390,340 Z" fill="url(#bodyGrad)" />
|
||||
<line x1="465" y1="435" x2="525" y2="565" stroke="#ff9900" stroke-width="9" stroke-linecap="round" />
|
||||
<path d="M 525,565 L 560,575 L 575,595 L 540,585 Z" fill="#ffaa00" />
|
||||
</g>
|
||||
|
||||
<!-- 鹈鹕右翼 (握住车把的翅膀,最前层) -->
|
||||
<g id="pelican-wing">
|
||||
<!-- 基础翅膀形态 -->
|
||||
<path d="M 350,240 C 440,250 560,260 600,270 C 590,290 500,310 400,320 C 300,330 200,320 220,290 Z" fill="url(#bodyGrad)" />
|
||||
|
||||
<!-- 初级飞羽 (深色) -->
|
||||
<path d="M 220,290 C 180,290 120,300 90,320 C 120,330 180,320 220,310 Z" fill="#222222" />
|
||||
<path d="M 230,300 C 180,310 110,330 80,350 C 120,355 180,340 230,320 Z" fill="#3a3a3a" />
|
||||
<path d="M 250,310 C 200,320 130,350 100,380 C 140,380 200,360 250,330 Z" fill="#4d4d4d" />
|
||||
|
||||
<!-- 次级飞羽 -->
|
||||
<path d="M 300,310 C 260,330 200,360 180,380 C 220,370 270,340 320,320 Z" fill="#888888" />
|
||||
<path d="M 350,315 C 310,340 250,370 230,390 C 270,380 320,350 370,325 Z" fill="#aaaaaa" />
|
||||
|
||||
<!-- 肩羽覆羽 (亮白) -->
|
||||
<path d="M 400,260 C 450,270 520,270 580,280 C 560,290 480,300 400,290 C 350,280 320,270 350,260 Z" fill="#ffffff" />
|
||||
|
||||
<!-- 翅膀轮廓与羽毛纹理 -->
|
||||
<path d="M 350,240 C 440,250 560,260 600,270" fill="none" stroke="#e0e0e0" stroke-width="2" />
|
||||
<path d="M 420,285 Q 470,285 520,290" fill="none" stroke="#cccccc" stroke-width="2" />
|
||||
<path d="M 380,300 Q 430,300 480,305" fill="none" stroke="#bbbbbb" stroke-width="2" />
|
||||
</g>
|
||||
|
||||
<!-- 动感特效:后轮扬起的沙粒与速度线 -->
|
||||
<g id="action-effects">
|
||||
<!-- 速度线 -->
|
||||
<line x1="120" y1="520" x2="220" y2="520" stroke="#ffffff" stroke-width="3" opacity="0.5" stroke-dasharray="15 8" />
|
||||
<line x1="80" y1="560" x2="200" y2="560" stroke="#b99e71" stroke-width="4" opacity="0.6" stroke-dasharray="25 15" />
|
||||
|
||||
<!-- 扬起的沙子 -->
|
||||
<g fill="#c2a77a">
|
||||
<circle cx="180" cy="580" r="4" />
|
||||
<circle cx="160" cy="595" r="5" />
|
||||
<circle cx="140" cy="575" r="2.5" />
|
||||
<circle cx="195" cy="605" r="3" />
|
||||
<circle cx="150" cy="615" r="4.5" />
|
||||
<circle cx="130" cy="590" r="2" />
|
||||
<circle cx="170" cy="610" r="3.5" />
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- 整体氛围光 (屏幕混合叠加,增加温馨感) -->
|
||||
<polygon points="800,220 -200,800 200,800 800,220" fill="url(#sunGrad)" opacity="0.25" style="mix-blend-mode: screen;" />
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 17 KiB |
Reference in new issue
Block a user