SOURCE

// ------------------- 颜色常量 -------------------
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 命令行工具 X clear

                    
>
console