// ------------------- 颜色常量 -------------------
const HEX_BG = '#5fa2e0';
const HEX_SUN = '#ffffff';
const HEX_WATER_BASE = '#2f7fd3';
const HEX_WATER_LIGHT = '#ffffff';
const HEX_WATER_DARK = '#1e5fa8';
const HEX_BOAT_HULL = '#1a2a3a';
const HEX_BOAT_SAIL = '#ffffff';
const HEX_BOAT_REF = '#2d5f9a';
// 远山颜色
const MOUNTAIN_COLORS = ['#b8d4f0', '#7ba9d4', '#4e7fb0'];
const SEA_TOP = 310;
const DURATION = 35000; // 35秒
// 月亮移动范围:从 (80,80) 移动到 (120,120)
const MOON_START = { x: 80, y: 80 };
const MOON_END = { x: 120, y: 120 };
let reflectChars = [];
let startTime;
let boatStartX = -80;
let boatEndX = 530;
let boatY;
let isAnimating = true;
let waveAmp = 6;
// 存储预先生成的山脉顶点
let mountainVertices = [[], [], []];
// ------------------- 字符海浪类 -------------------
class ReflectChar {
constructor() {
let t = pow(random(), 1.6);
this.y = map(t, 0, 1, SEA_TOP, height - 20);
this.x = random(-width * 0.3, width * 1.3);
this.char = this.getRandomSymbol();
this.colorType = floor(random(2));
let depth = map(this.y, SEA_TOP, height, 0, 1);
this.textSize = map(depth, 0, 1, 6, 16);
this.speed = map(depth, 0, 1, 0.8, 2.8);
this.alphaBase = map(depth, 0, 1, 100, 240);
}
getRandomSymbol() {
let symbols = ['·', '~', '*', '.', '°', '○', '●', '◌', '◍', '◎', '◦', '◯', '∴', '∵', '〜'];
return random(symbols);
}
update(progress) {
if (progress < 1.0) {
this.x += this.speed;
if (this.x > width + 100) this.reset();
}
}
reset() {
this.x = -random(60, 90);
let t = pow(random(), 1.6);
this.y = map(t, 0, 1, SEA_TOP, height - 20);
this.char = this.getRandomSymbol();
this.colorType = floor(random(2));
let depth = map(this.y, SEA_TOP, height, 0, 1);
this.textSize = map(depth, 0, 1, 6, 16);
this.speed = map(depth, 0, 1, 0.8, 2.8);
this.alphaBase = map(depth, 0, 1, 100, 240);
}
display(boatX, boatY, progress) {
let flicker = 0.7 + 0.6 * sin(frameCount * 0.08 + (this.x + this.y) * 0.02);
let finalAlpha = constrain(this.alphaBase * flicker, 40, 255);
let isUnderBoat = abs(this.x - boatX) < 42 && this.y > boatY + 2 && this.y < boatY + 55;
let col;
if (isUnderBoat) {
col = color(HEX_BOAT_REF);
} else {
col = this.colorType == 0 ? color(HEX_WATER_LIGHT) : color(HEX_WATER_DARK);
}
textSize(this.textSize);
textAlign(CENTER, CENTER);
fill(red(col), green(col), blue(col), finalAlpha);
let yOffset = sin(frameCount * 0.06 + this.x * 0.04) * 2;
text(this.char, this.x, this.y + yOffset);
}
}
// ------------------- 预生成山脉顶点 -------------------
function generateMountainVertices(baseY, amplitude, freq, tilt, noiseOffset) {
let vertices = [];
let bottomY = SEA_TOP + 20;
vertices.push(createVector(0, bottomY));
vertices.push(createVector(0, baseY));
for (let x = 0; x <= width; x += 5) {
let n = noise(noiseOffset + x * freq, baseY * 0.015);
let offset = n * amplitude;
let tiltY = map(x, 0, width, tilt, 0);
let y = baseY - offset - tiltY;
vertices.push(createVector(x, y));
}
vertices.push(createVector(width, baseY));
vertices.push(createVector(width, bottomY));
return vertices;
}
function drawMountainFromVertices(vertices, col) {
push();
fill(col);
noStroke();
beginShape();
for (let v of vertices) vertex(v.x, v.y);
endShape(CLOSE);
pop();
}
// ------------------- 帆船绘制 -------------------
function drawBoat(x, y, progress) {
let bob = (progress < 1.0) ? sin(frameCount * 0.07) * waveAmp : 0;
push();
translate(x, y + bob);
noStroke();
fill(HEX_BOAT_HULL);
quad(-40, 0, 40, 0, 22, 14, -22, 14);
fill(HEX_BOAT_SAIL);
triangle(-28, -8, -8, -58, -8, -2);
triangle(5, -60, 32, -8, 0, -8);
pop();
}
// ------------------- 绘制月亮(带光晕,位置动态移动)------------------
function drawMoon(progress) {
// 根据进度插值月亮中心位置
let moonX = lerp(MOON_START.x, MOON_END.x, progress);
let moonY = lerp(MOON_START.y, MOON_END.y, progress);
push();
drawingContext.shadowBlur = 28;
drawingContext.shadowColor = color(255, 255, 200, 150);
fill(HEX_SUN);
noStroke();
ellipse(moonX, moonY, 44, 44);
// 外光晕
fill(255, 255, 220, 70);
ellipse(moonX, moonY, 70, 70);
pop();
}
// ------------------- p5.js 核心 -------------------
function setup() {
createCanvas(450, 600);
pixelDensity(1);
for (let i = 0; i < 400; i++) reflectChars.push(new ReflectChar());
noiseSeed(42);
frameRate(30);
startTime = millis();
boatY = SEA_TOP + 45;
mountainVertices[0] = generateMountainVertices(SEA_TOP - 48, 42, 0.008, -5, 100);
mountainVertices[1] = generateMountainVertices(SEA_TOP - 32, 48, 0.012, 4, 200);
mountainVertices[2] = generateMountainVertices(SEA_TOP - 18, 52, 0.016, -2, 300);
}
function draw() {
let elapsed = millis() - startTime;
let progress = min(1.0, elapsed / DURATION);
// 1. 天空
background(HEX_BG);
// 2. 月亮(动态移动)
drawMoon(progress);
// 3. 山脉(固定形状)
drawMountainFromVertices(mountainVertices[0], MOUNTAIN_COLORS[0]);
drawMountainFromVertices(mountainVertices[1], MOUNTAIN_COLORS[1]);
drawMountainFromVertices(mountainVertices[2], MOUNTAIN_COLORS[2]);
// 4. 海面底色
fill(HEX_WATER_BASE);
noStroke();
rect(0, SEA_TOP, width, height - SEA_TOP);
// 5. 字符海浪
for (let c of reflectChars) {
c.update(progress);
let currentBoatX = lerp(boatStartX, boatEndX, progress);
c.display(currentBoatX, boatY, progress);
}
// 6. 帆船
let currentBoatX = lerp(boatStartX, boatEndX, progress);
drawBoat(currentBoatX, boatY, progress);
// 7. 胶片颗粒
loadPixels();
let grain = 28;
for (let i = 0; i < 800; i++) {
let x = floor(random(width));
let y = floor(random(height));
let idx = (x + y * width) * 4;
let delta = random(-grain, grain);
pixels[idx] = constrain(pixels[idx] + delta, 0, 255);
pixels[idx+1] = constrain(pixels[idx+1] + delta, 0, 255);
pixels[idx+2] = constrain(pixels[idx+2] + delta, 0, 255);
}
updatePixels();
// 8. 动画结束停止
if (progress >= 1.0 && isAnimating) {
isAnimating = false;
noLoop();
}
}
function windowResized() {
resizeCanvas(450, 600);
}
* {
margin: 0;
}
console