
const TRI_SIZE = 30;
const SPACING = 30;
const BRIGHT_RANGE = 600;
const ROTATION_EASING = 0.1;
const HUE_MIN = 180;
const HUE_MAX = 240;
const BRIGHTNESS_NEAR = 90;
const BRIGHTNESS_FAR = 10;
let grid;
function setup() {
createCanvas(windowWidth, windowHeight);
colorMode(HSB, 360, 100, 100);
grid = new TriangleGrid();
}
function draw() {
background(0);
grid.update();
grid.display();
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
grid = new TriangleGrid();
}
class TriangleGrid {
constructor() {
this.cols = ceil(width / SPACING);
this.rows = ceil(height / SPACING);
this.triangles = [];
this.initializeTriangles();
}
initializeTriangles() {
for (let i = 0; i < this.cols; i++) {
for (let j = 0; j < this.rows; j++) {
let x = SPACING / 2 + i * SPACING;
let y = SPACING / 2 + j * SPACING;
this.triangles.push(new Triangle(x, y));
}
}
}
update() {
for (let tri of this.triangles) {
tri.update();
}
}
display() {
for (let tri of this.triangles) {
tri.display();
}
}
}
class Triangle {
constructor(x, y) {
this.x = x;
this.y = y;
this.currentAngle = 0;
}
update() {
let targetAngle = atan2(mouseY - this.y, mouseX - this.x);
let diff = targetAngle - this.currentAngle;
if (diff > PI) diff -= TWO_PI;
if (diff < -PI) diff += TWO_PI;
this.currentAngle += diff * ROTATION_EASING;
}
display() {
let distance = dist(mouseX, mouseY, this.x, this.y);
let hue = map(distance, 0, BRIGHT_RANGE, HUE_MIN, HUE_MAX, true);
let brightness;
if (distance < BRIGHT_RANGE) {
let t = distance / BRIGHT_RANGE;
let easedT = 1 - pow(1 - t, 3);
brightness = lerp(BRIGHTNESS_NEAR, BRIGHTNESS_FAR, easedT);
} else {
brightness = BRIGHTNESS_FAR;
}
noStroke();
fill(hue, 100, brightness);
push();
translate(this.x, this.y);
rotate(this.currentAngle);
beginShape();
vertex(TRI_SIZE / 2, 0);
vertex(-TRI_SIZE / 2, -TRI_SIZE / 6);
vertex(-TRI_SIZE / 2, TRI_SIZE / 6);
endShape(CLOSE);
pop();
}
}
// ======================================================
// 1. ์ ์ญ ์ค์ ๊ฐ (Configuration Constants)
// ======================================================
// ์ผ๊ฐํ ๊ทธ๋ฆฌ๋ ๊ธฐ๋ณธ ์ค์
const TRI_SIZE = 20; // ์ผ๊ฐํ ํฌ๊ธฐ
const SPACING = 30; // ์ผ๊ฐํ ๊ฐ๊ฒฉ
const BRIGHT_RANGE = 500; // ๋ฐ์์ง๋ ํจ๊ณผ ๋ฒ์ (ํฝ์
)
const ROTATION_EASING = 0.1; // ํ์ easing ๊ฐ (0~1, ์์์๋ก ๋ถ๋๋ฌ์)
// HSB ์์ ์ค์
const HUE_MIN = 180; // Hue ์ต์๊ฐ (0~360, ์ฒญ๋ก์)
const HUE_MAX = 240; // Hue ์ต๋๊ฐ (0~360, ํ๋์)
const BRIGHTNESS_NEAR = 80; // ๊ฐ๊น์ด ๊ณณ์ ๋ฐ๊ธฐ (0~100)
const BRIGHTNESS_FAR = 20; // ๋จผ ๊ณณ์ ๋ฐ๊ธฐ (0~100)
// ======================================================
// 2. ์ ์ญ ์ํ ๋ณ์ (Global State)
// ======================================================
let grid; // ์ผ๊ฐํ ๊ทธ๋ฆฌ๋ ๊ฐ์ฒด (TriangleGrid ์ธ์คํด์ค)
// ======================================================
// 3. p5.js ๋ผ์ดํ์ฌ์ดํด ํจ์
// ======================================================
// p5.js ์ด๊ธฐ ์ค์ ํจ์ (ํ๋ก๊ทธ๋จ ์์ ์ ํ ๋ฒ๋ง ์คํ)
function setup() {
// ์บ๋ฒ์ค ์์ฑ (์ ์ฒด ์๋์ฐ ํฌ๊ธฐ)
createCanvas(windowWidth, windowHeight);
// ์์ ๋ชจ๋๋ฅผ HSB๋ก ์ค์
// hue: 0~360, saturation/brightness: 0~100
colorMode(HSB, 360, 100, 100);
// ์ผ๊ฐํ ๊ทธ๋ฆฌ๋ ์ด๊ธฐํ
grid = new TriangleGrid();
}
// p5.js ๋ฉ์ธ ๋ฃจํ ํจ์ (๋งค ํ๋ ์๋ง๋ค ์คํ)
function draw() {
// ๋ฐฐ๊ฒฝ์ ๊ฒ์์์ผ๋ก ์ด๊ธฐํ
background(0);
// ๋ชจ๋ ์ผ๊ฐํ์ ์ํ ์
๋ฐ์ดํธ (๊ฐ๋ ๊ณ์ฐ)
grid.update();
// ๋ชจ๋ ์ผ๊ฐํ ํ๋ฉด์ ๊ทธ๋ฆฌ๊ธฐ
grid.display();
}
// ์๋์ฐ ํฌ๊ธฐ ๋ณ๊ฒฝ ์ ํธ์ถ๋๋ ํจ์
function windowResized() {
// ์บ๋ฒ์ค ํฌ๊ธฐ ์ฌ์กฐ์
resizeCanvas(windowWidth, windowHeight);
// ๊ทธ๋ฆฌ๋ ์ฌ์์ฑ
grid = new TriangleGrid();
}
// ======================================================
// 4. ํด๋์ค ์ ์ (Class Definitions)
// ======================================================
// ์ผ๊ฐํ ๊ทธ๋ฆฌ๋๋ฅผ ๊ด๋ฆฌํ๋ ํด๋์ค
class TriangleGrid {
constructor() {
// ํ๋ฉด์ ํ์ํ ์ด(column)๊ณผ ํ(row) ๊ฐ์ ๊ณ์ฐ
this.cols = ceil(width / SPACING);
this.rows = ceil(height / SPACING);
// ๋ชจ๋ ์ผ๊ฐํ ๊ฐ์ฒด๋ฅผ ๋ด์ ๋ฐฐ์ด
this.triangles = [];
// ์ผ๊ฐํ ๊ฐ์ฒด๋ค ์์ฑ ๋ฐ ์ด๊ธฐํ
this.initializeTriangles();
}
// ๊ทธ๋ฆฌ๋ ์์ ๋ชจ๋ ์์น์ ์ผ๊ฐํ ๊ฐ์ฒด ์์ฑ
initializeTriangles() {
for (let i = 0; i < this.cols; i++) {
for (let j = 0; j < this.rows; j++) {
// ๊ฐ ์ผ๊ฐํ์ ํ๋ฉด ์ ์ขํ ๊ณ์ฐ
let x = SPACING / 2 + i * SPACING;
let y = SPACING / 2 + j * SPACING;
// Triangle ์ธ์คํด์ค ์์ฑ ํ ๋ฐฐ์ด์ ์ถ๊ฐ
this.triangles.push(new Triangle(x, y));
}
}
}
// ๋ชจ๋ ์ผ๊ฐํ์ ์ํ ์
๋ฐ์ดํธ (๊ฐ๋ ๊ณ์ฐ)
update() {
for (let tri of this.triangles) {
tri.update();
}
}
// ๋ชจ๋ ์ผ๊ฐํ์ ํ๋ฉด์ ๊ทธ๋ฆฌ๊ธฐ
display() {
for (let tri of this.triangles) {
tri.display();
}
}
}
// ๊ฐ๋ณ ์ผ๊ฐํ์ ๋ํ๋ด๋ ํด๋์ค
class Triangle {
constructor(x, y) {
this.x = x; // ์ผ๊ฐํ์ X ์ขํ
this.y = y; // ์ผ๊ฐํ์ Y ์ขํ
this.currentAngle = 0; // ํ์ฌ ํ์ ๊ฐ๋ (๋ผ๋์)
}
// ์ผ๊ฐํ์ ํ์ ๊ฐ๋๋ฅผ ๋ง์ฐ์ค ๋ฐฉํฅ์ผ๋ก ์
๋ฐ์ดํธ
update() {
// ๋ง์ฐ์ค๋ฅผ ํฅํ๋ ๋ชฉํ ๊ฐ๋ ๊ณ์ฐ
let targetAngle = atan2(mouseY - this.y, mouseX - this.x);
// ํ์ฌ ๊ฐ๋์ ๋ชฉํ ๊ฐ๋์ ์ฐจ์ด ๊ณ์ฐ
let diff = targetAngle - this.currentAngle;
// ๊ฐ๋ ์ฐจ์ด๋ฅผ -PI ~ PI ๋ฒ์๋ก ์ ๊ทํ
// (์ต๋จ ๊ฒฝ๋ก๋ก ํ์ ํ๋๋ก ๋ณด์ )
if (diff > PI) diff -= TWO_PI;
if (diff < -PI) diff += TWO_PI;
// easing์ ์ ์ฉํ์ฌ ๋ถ๋๋ฝ๊ฒ ํ์
this.currentAngle += diff * ROTATION_EASING;
}
// ์ผ๊ฐํ์ ํ๋ฉด์ ๊ทธ๋ฆฌ๊ธฐ
display() {
// ๋ง์ฐ์ค์ ์ผ๊ฐํ ์ฌ์ด์ ๊ฑฐ๋ฆฌ ๊ณ์ฐ
let distance = dist(mouseX, mouseY, this.x, this.y);
// ๊ฑฐ๋ฆฌ์ ๋ฐ๋ฅธ Hue ๊ฐ ๊ณ์ฐ
// ๊ฐ๊น์ฐ๋ฉด HUE_MIN, ๋ฉ๋ฉด HUE_MAX
let hue = map(distance, 0, BRIGHT_RANGE, HUE_MIN, HUE_MAX, true);
// ๊ฑฐ๋ฆฌ์ ๋ฐ๋ฅธ ๋ฐ๊ธฐ ๊ณ์ฐ (ease-out cubic ์ ์ฉ)
let brightness;
if (distance < BRIGHT_RANGE) {
// 0~1 ์ฌ์ด๋ก ์ ๊ทํ
let t = distance / BRIGHT_RANGE;
// ease-out cubic ํจ์: 1 - (1-t)ยณ
// ๊ฐ๊น์ธ ๋๋ ์ฒ์ฒํ ๋ณํ, ๋ฉ ๋๋ ๊ธ๊ฒฉํ ๋ณํ
let easedT = 1 - pow(1 - t, 3);
// ์ ๊ทํ๋ ๊ฐ์ ์ค์ ๋ฐ๊ธฐ ๊ฐ์ผ๋ก ๋ณํ
brightness = lerp(BRIGHTNESS_NEAR, BRIGHTNESS_FAR, easedT);
} else {
// ๋ฒ์๋ฅผ ๋ฒ์ด๋ ๊ฒฝ์ฐ ๋จผ ๊ณณ์ ๋ฐ๊ธฐ ์ ์ฉ
brightness = BRIGHTNESS_FAR;
}
// ์ธ๊ณฝ์ ์์ด ์์๋ง ์ฑ์ฐ๊ธฐ
noStroke();
fill(hue, 100, brightness);
// ์ผ๊ฐํ ๊ทธ๋ฆฌ๊ธฐ
push();
translate(this.x, this.y); // ์ผ๊ฐํ ์์น๋ก ์ด๋
rotate(this.currentAngle); // ๊ณ์ฐ๋ ๊ฐ๋๋ก ํ์
beginShape();
vertex(TRI_SIZE / 2, 0); // ์ค๋ฅธ์ชฝ ๋ (์์ชฝ)
vertex(-TRI_SIZE / 2, -TRI_SIZE / 4); // ์ผ์ชฝ ์๋
vertex(-TRI_SIZE / 2, TRI_SIZE / 4); // ์ผ์ชฝ ์
endShape(CLOSE);
pop();
}
}