そーまの電子工作 Blog

				
					<meta name="viewport" content="width=device-width, initial-scale=1">

<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.min.js"></script>

<div id="p5container" style="width:100%; height:520px;"></div>

<script>
let DEVICE_TYPE;
let PARAMS;

function detectDevice() {
  const ua = navigator.userAgent;
  if (/iPhone|iPad|iPod/i.test(ua)) return 'iphone';
  if (/Android/i.test(ua)) return 'android';
  return 'desktop';
}

function initParams() {
  DEVICE_TYPE = detectDevice();

  const IPHONE = {
    RESOLUTION_SCALE: 15,
    VIEW_ZOOM: 0.00005,
    AGENT_COUNT: 10000,
    PARTICLE_SIZE: 0.00005,
    noiseScale: 7899550,
    Z_SPEED: 0.0019,
    SPEED_MIN: 0,
    SPEED_MAX: 195000.4,
    MOMENTUM: 0.3,
    SPREAD_NOISE: 0.03,
    SPREAD_SCALE: 0.01,
    HUE_SPEED: 600,
    SAT_SPEED: 55,
    BRI_SPEED: 105,
    BG_H: 0,
    BG_S: 0,
    BG_B: 0,
    HUE_START_OFFSET: 500,

    // 線の透明度
    // 0 = 完全透明 / 100 = 完全不透明
    LINE_ALPHA: 20
  };

  const ANDROID = {
    RESOLUTION_SCALE: 2,
    VIEW_ZOOM: 0.07,
    AGENT_COUNT: 4000,
    PARTICLE_SIZE: 0.013,
    noiseScale: 6000,
    Z_SPEED: 0.00045,
    SPEED_MIN: 2.2,
    SPEED_MAX: 3.4,
    MOMENTUM: 0.9,
    SPREAD_NOISE: 0.003,
    SPREAD_SCALE: 14000,
    HUE_SPEED: 2600,
    SAT_SPEED: 140,
    BRI_SPEED: 200,
    BG_H: 0,
    BG_S: 0,
    BG_B: 0,
    HUE_START_OFFSET: 2500,

    // 線の透明度
    LINE_ALPHA: 100
  };

  const DESKTOP = {
    RESOLUTION_SCALE: 5,
    VIEW_ZOOM: 0.08,
    AGENT_COUNT: 12000,
    PARTICLE_SIZE: 0.01,
    noiseScale: 7000,
    Z_SPEED: 0.0004,
    SPEED_MIN: 2.8,
    SPEED_MAX: 4.3,
    MOMENTUM: 0.91,
    SPREAD_NOISE: 0.003,
    SPREAD_SCALE: 16000,
    HUE_SPEED: 2600,
    SAT_SPEED: 140,
    BRI_SPEED: 200,
    BG_H: 0,
    BG_S: 0,
    BG_B: 0,
    HUE_START_OFFSET: 1500,

    // 線の透明度
    LINE_ALPHA: 100
  };

  if (DEVICE_TYPE === 'iphone') PARAMS = IPHONE;
  else if (DEVICE_TYPE === 'android') PARAMS = ANDROID;
  else PARAMS = DESKTOP;
}

let agents = [];
let zoff = 10;
let lastWrapW = 0;
let lastWrapH = 0;
let tapStartX = 0;
let tapStartY = 0;
let tapStartTime = 0;
let tapMoved = false;

class Agent {
  constructor() {
    this.x = 0;
    this.y = 0;
    this.vecx = 0;
    this.vecy = 0;
    this.count = 0;
    this.speed = 0;
    this.col = null;
  }
}

function setup() {
  initParams();

  pixelDensity(PARAMS.RESOLUTION_SCALE);
  colorMode(HSB, 360, 100, 100, 100);

  const wrap = document.getElementById('p5container');
  const c = createCanvas(wrap.offsetWidth, wrap.offsetHeight);

  c.parent('p5container');
  c.canvas.style.transform = 'translateZ(0)';

  lastWrapW = wrap.offsetWidth;
  lastWrapH = wrap.offsetHeight;

  setupTapReset(c.canvas);
  background(PARAMS.BG_H, PARAMS.BG_S, PARAMS.BG_B);
  noFill();
  resetAgents();

  if (DEVICE_TYPE === 'iphone') {
    frameRate(24);
  } else {
    frameRate(600);
  }
}

function setupTapReset(canvas) {
  canvas.addEventListener('pointerdown', function(e) {
    tapStartX = e.clientX;
    tapStartY = e.clientY;
    tapStartTime = performance.now();
    tapMoved = false;
  }, { passive: true });

  canvas.addEventListener('pointermove', function(e) {
    const dx = e.clientX - tapStartX;
    const dy = e.clientY - tapStartY;
    if (Math.sqrt(dx * dx + dy * dy) > 12) tapMoved = true;
  }, { passive: true });

  canvas.addEventListener('pointerup', function(e) {
    const dx = e.clientX - tapStartX;
    const dy = e.clientY - tapStartY;
    const dist = Math.sqrt(dx * dx + dy * dy);
    const elapsed = performance.now() - tapStartTime;

    if (!tapMoved && dist <= 12 && elapsed <= 450) {
      background(PARAMS.BG_H, PARAMS.BG_S, PARAMS.BG_B);
      resetAgents();
    }
  }, { passive: true });

  canvas.addEventListener('pointercancel', function() {
    tapMoved = true;
  }, { passive: true });
}

function resetAgents() {
  agents = [];

  const worldW = width / PARAMS.VIEW_ZOOM;
  const worldH = height / PARAMS.VIEW_ZOOM;

  for (let i = 0; i < PARAMS.AGENT_COUNT; i++) {
    const agent = new Agent();

    agent.x = random(0, worldW);
    agent.y = random(0, worldH);
    agent.vecx = random(-1, 1);
    agent.vecy = random(-1, 1);
    agent.speed = random(PARAMS.SPEED_MIN, PARAMS.SPEED_MAX);

    agent.col = color(0, 100, 100, PARAMS.LINE_ALPHA);

    agents.push(agent);
  }
}

function draw() {
  zoff += PARAMS.Z_SPEED;

  const worldW = width / PARAMS.VIEW_ZOOM;
  const worldH = height / PARAMS.VIEW_ZOOM;

  push();
  scale(PARAMS.VIEW_ZOOM);

  const nextAgents = [];

  for (const agent of agents) {
    const next = new Agent();
    next.speed = agent.speed;

    const angle = map(
      noise(
        agent.x / PARAMS.noiseScale,
        agent.y / PARAMS.noiseScale,
        zoff
      ),
      0,
      1,
      -PI,
      PI
    );

    const spreadAngle =
      noise(
        agent.x / PARAMS.SPREAD_SCALE,
        agent.y / PARAMS.SPREAD_SCALE,
        zoff + 100
      ) * TWO_PI * PARAMS.SPREAD_NOISE;

    next.vecx =
      agent.vecx * PARAMS.MOMENTUM +
      cos(angle * TWO_PI + spreadAngle) * agent.speed;

    next.vecy =
      agent.vecy * PARAMS.MOMENTUM +
      sin(angle * TWO_PI + spreadAngle) * agent.speed;

    next.x = agent.x + next.vecx;
    next.y = agent.y + next.vecy;
    next.count = agent.count + 1;

    if (
      agent.x <= 0 ||
      agent.x >= worldW ||
      agent.y <= 0 ||
      agent.y >= worldH
    ) {
      next.x = random(0, worldW);
      next.y = random(0, worldH);
      next.vecx = random(-1, 1);
      next.vecy = random(-1, 1);
      next.speed = random(PARAMS.SPEED_MIN, PARAMS.SPEED_MAX);
    } else {
      stroke(agent.col);
      strokeWeight(PARAMS.PARTICLE_SIZE / PARAMS.VIEW_ZOOM);
      line(agent.x, agent.y, next.x, next.y);
    }

    next.col = color(
      sin((frameCount + PARAMS.HUE_START_OFFSET) / PARAMS.HUE_SPEED) * 110 + 70,
      sin(frameCount / PARAMS.SAT_SPEED) * 40 + 60,
      sin(frameCount / PARAMS.BRI_SPEED) * 40 + 60 + random(5, 8),
      PARAMS.LINE_ALPHA
    );

    nextAgents.push(next);
  }

  agents = nextAgents;

  pop();
}

function windowResized() {
  const wrap = document.getElementById('p5container');
  if (!wrap) return;

  const newW = wrap.offsetWidth;
  const newH = wrap.offsetHeight;

  const widthChanged = Math.abs(newW - lastWrapW) > 2;
  const heightChangedLarge = Math.abs(newH - lastWrapH) > 80;
  const IS_MOBILE = DEVICE_TYPE === 'iphone' || DEVICE_TYPE === 'android';

  if (IS_MOBILE && !widthChanged && !heightChangedLarge) return;
  if (!IS_MOBILE && !widthChanged && Math.abs(newH - lastWrapH) <= 2) return;

  lastWrapW = newW;
  lastWrapH = newH;

  resizeCanvas(newW, newH);
  background(PARAMS.BG_H, PARAMS.BG_S, PARAMS.BG_B);
  resetAgents();
}
</script>