PageSourceSearch

https://micelink.com/js/hero-bg.js

js micelink.com collected 2026-10-09 17:30:26 UTC 8,280 bytes, 219 lines download raw bytes

1/* MICELINK 히어로 배경 — Three.js "MICE 시티 토폴로지" (ES module, CDN 로드)
2   물결치는 와이어 그리드 지형 위에 컨벤션센터·호텔·수송 허브를 육각 프리즘으로 세우고,
3   허브 사이를 아크로 연결해 예약 패킷이 흐른다. 마우스 패럴랙스 + 드래그 회전. */
4import * as THREE from "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js";
5
6/* 그라디언트 스카이 위에 화이트 라인 도시로 렌더 (Micro 스타일) */
7const WHITE = 0xffffff;
8const INK = 0x221f1c;
9const PRIMARY = WHITE;
10const PRIMARY_DARK = WHITE;
11const PRIMARY_LIGHT = WHITE;
12const PRIMARY_SOFT = WHITE;
13
14/* 허브: 컨벤션센터(중앙) + 호텔/수송 허브 (x, z, 높이, 색) */
15const HUBS = [
16  { x: 0, z: 0, h: 96, color: PRIMARY_DARK },        // 컨벤션센터
17  { x: 120, z: -80, h: 76, color: PRIMARY },          // 호텔 A
18  { x: -130, z: -60, h: 68, color: PRIMARY },         // 호텔 B
19  { x: 170, z: 90, h: 60, color: PRIMARY_LIGHT },     // 수송 터미널
20  { x: -160, z: 120, h: 50, color: PRIMARY_LIGHT },   // 공항 셔틀 허브
21  { x: 60, z: 160, h: 56, color: PRIMARY },           // 호텔 C
22  { x: -70, z: -160, h: 58, color: PRIMARY_SOFT },    // 리조트
23];
24
25function init(host) {
26  const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
27  const isMobile = window.matchMedia("(max-width: 640px)").matches;
28
29  const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: "low-power" });
30  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
31  renderer.setClearColor(0x000000, 0);
32  host.appendChild(renderer.domElement);
33
34  const scene = new THREE.Scene();
35
36  const camera = new THREE.PerspectiveCamera(45, 1, 1, 3000);
37  camera.position.set(0, 160, 480);
38
39  scene.add(new THREE.AmbientLight(0xffffff, 0.9));
40  const key = new THREE.DirectionalLight(0xffffff, 0.9);
41  key.position.set(200, 400, 200);
42  scene.add(key);
43  const fill = new THREE.DirectionalLight(0xcff2ef, 0.5);
44  fill.position.set(-200, 250, -100);
45  scene.add(fill);
46
47  const group = new THREE.Group();
48  group.rotation.x = 0.38;
49  scene.add(group);
50
51  /* ---- 물결 그리드 지형 ---- */
52  const SIZE = 900;
53  const SEG = isMobile ? 32 : 48;
54  const gridGeo = new THREE.PlaneGeometry(SIZE, SIZE, SEG, SEG);
55  gridGeo.rotateX(-Math.PI / 2);
56  const gridOrig = new Float32Array(gridGeo.attributes.position.array);
57  const grid = new THREE.Mesh(
58    gridGeo,
59    new THREE.MeshBasicMaterial({ color: WHITE, wireframe: true, transparent: true, opacity: 0.28 })
60  );
61  group.add(grid);
62
63  const groundGeo = new THREE.PlaneGeometry(SIZE, SIZE);
64  groundGeo.rotateX(-Math.PI / 2);
65  const ground = new THREE.Mesh(groundGeo, new THREE.MeshBasicMaterial({ color: WHITE, transparent: true, opacity: 0.1 }));
66  ground.position.y = -6;
67  group.add(ground);
68
69  /* ---- 허브 건물 (육각 프리즘 + 흰 엣지 + 비콘 + 지면 링) ---- */
70  const nodes = [];
71  HUBS.forEach((hub) => {
72    const prismGeo = new THREE.CylinderGeometry(14, 18, hub.h, 6);
73    const prism = new THREE.Mesh(
74      prismGeo,
75      new THREE.MeshStandardMaterial({ color: WHITE, roughness: 0.35, metalness: 0, transparent: true, opacity: 0.92 })
76    );
77    prism.position.set(hub.x, hub.h / 2, hub.z);
78    prism.add(new THREE.LineSegments(new THREE.EdgesGeometry(prismGeo), new THREE.LineBasicMaterial({ color: INK, transparent: true, opacity: 0.35 })));
79    group.add(prism);
80
81    const beacon = new THREE.Mesh(new THREE.SphereGeometry(3.5, 16, 16), new THREE.MeshBasicMaterial({ color: hub.color }));
82    beacon.position.set(hub.x, hub.h + 8, hub.z);
83    group.add(beacon);
84
85    const ringGeo = new THREE.RingGeometry(22, 25, 40);
86    ringGeo.rotateX(-Math.PI / 2);
87    const ring = new THREE.Mesh(ringGeo, new THREE.MeshBasicMaterial({ color: hub.color, side: THREE.DoubleSide, transparent: true, opacity: 0.35 }));
88    ring.position.set(hub.x, 0.8, hub.z);
89    group.add(ring);
90
91    nodes.push({ pos: new THREE.Vector3(hub.x, hub.h + 8, hub.z), ring });
92  });
93
94  /* ---- 허브 연결 아크 + 이동 패킷 ---- */
95  const packets = [];
96  const center = nodes[0].pos;
97  for (let i = 1; i < nodes.length; i++) {
98    const target = nodes[i].pos;
99    const mid = new THREE.Vector3().addVectors(center, target).multiplyScalar(0.5);
100    mid.y += 50;
101    const curve = new THREE.QuadraticBezierCurve3(center, mid, target);
102    const line = new THREE.Line(
103      new THREE.BufferGeometry().setFromPoints(curve.getPoints(40)),
104      new THREE.LineBasicMaterial({ color: WHITE, transparent: true, op
104acity: 0.7 })
105    );
106    group.add(line);
107
108    const packet = new THREE.Mesh(new THREE.SphereGeometry(3, 12, 12), new THREE.MeshBasicMaterial({ color: INK }));
109    group.add(packet);
110    packets.push({ mesh: packet, curve, t: Math.random(), speed: 0.12 + Math.random() * 0.08, dir: i % 2 ? 1 : -1 });
111  }
112
113  /* ---- 레이아웃 (텍스트가 좌측이라 도시를 우측으로) ---- */
114  function layout() {
115    const w = host.clientWidth || 1;
116    const h = host.clientHeight || 1;
117    renderer.setSize(w, h, false);
118    camera.aspect = w / h;
119    camera.updateProjectionMatrix();
120    if (w >= 960) group.position.set(150, -50, 0);
121    else group.position.set(0, -70, 0);
122  }
123  layout();
124  if ("ResizeObserver" in window) new ResizeObserver(layout).observe(host);
125  else window.addEventListener("resize", layout);
126
127  /* ---- 인터랙션: 마우스 패럴랙스 + 드래그 회전 ---- */
128  const mouse = { x: 0, y: 0 };
129  const eased = { x: 0, y: 0 };
130  let dragRot = 0;
131  let dragging = false;
132  let lastX = 0;
133
134  if (!reduceMotion) {
135    window.addEventListener("pointermove", (e) => {
136      mouse.x = (e.clientX / window.innerWidth - 0.5) * 0.9;
137      mouse.y = (e.clientY / window.innerHeight - 0.5) * 0.9;
138      if (dragging) {
139        dragRot += (e.clientX - lastX) * 0.004;
140        lastX = e.clientX;
141      }
142    }, { passive: true });
143  }
144  const canvas = renderer.domElement;
145  canvas.addEventListener("pointerdown", (e) => {
146    dragging = true;
147    lastX = e.clientX;
148    host.classList.add("is-dragging");
149  });
150  window.addEventListener("pointerup", () => {
151    dragging = false;
152    host.classList.remove("is-dragging");
153  });
154
155  /* ---- 렌더 루프 ---- */
156  let visible = true;
157  if ("IntersectionObserver" in window) {
158    new IntersectionObserver((entries) => { visible = entries[0].isIntersecting; }, { threshold: 0 }).observe(host);
159  }
160
161  const clock = new THREE.Clock();
162  const pos = gridGeo.attributes.position;
163  const arr = pos.array;
164
165  function render(time) {
166    if (!reduceMotion) {
167      group.rotation.y = Math.sin(time * 0.25) * 0.18 + dragRot + eased.x * 0.6;
168
169      for (let i = 0; i < arr.length; i += 3) {
170        const ox = gridOrig[i];
171        const oz = gridOrig[i + 2];
172        arr[i + 1] = Math.sin(ox * 0.015 + time * 1.5) * Math.cos(oz * 0.015 + time * 1.2) * 6.5;
173      }
174      pos.needsUpdate = true;
175
176      packets.forEach((p) => {
177        p.t = (p.t + p.speed * 0.016) % 1;
178        p.mesh.position.copy(p.curve.getPointAt(p.dir > 0 ? p.t : 1 - p.t));
179      });
180      nodes.forEach((n, idx) => {
181        const s = 1 + Math.sin(time * 2.5 + idx) * 0.08;
182        n.ring.scale.set(s, s, 1);
183      });
184    } else {
185      group.rotation.y = dragRot;
186      packets.forEach((p) => p.mesh.position.copy(p.curve.getPointAt(0.5)));
187    }
188
189    eased.x += (mouse.x - eased.x) * 0.04;
190    eased.y += (mouse.y - eased.y) * 0.04;
191    camera.position.x = eased.x * 130;
192    camera.position.y = 160 - eased.y * 80;
193    camera.lookAt(group.position.x * 0.3, 20, 0);
194
195    renderer.render(scene, camera);
196  }
197
198  function frame() {
199    requestAnimationFrame(frame);
200    if (!visible || document.hidden) return;
201    render(clock.getElapsedTime());
202  }
203
204  render(0);
205  host.classList.add("is-ready");
206  if (!reduceMotion) frame();
207  else canvas.addEventListener("pointermove", () => render(0));
208}
209
210/* 진입점 — 상단 상수 초기화 이후 실행 */
211const host = document.querySelector("[data-hero-bg]");
212if (host) {
213  try {
214    init(host);
215  } catch (err) {
216    // WebGL 미지원/차단 환경: 배경 없이 그라디언트만 유지
217    console.warn("[hero-bg] WebGL 초기화 실패, 정적 배경으로 대체합니다.", err);
218  }
219}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.