|
|
| (사용자 2명의 중간 판 6개는 보이지 않습니다) |
| 1번째 줄: |
1번째 줄: |
| <!DOCTYPE html>
| | [http://creeper0809.synology.me/] |
| <html lang="ko">
| | '''http로 접속할 것''' |
| <head>
| |
| <meta charset="UTF-8">
| |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
| |
| <title>Vibrant Math Tree</title>
| |
| <style>
| |
| @import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&display=swap');
| |
| body {
| |
| margin: 0;
| |
| overflow: hidden;
| |
| background-color: #020205;
| |
| font-family: 'Fira Code', monospace;
| |
| cursor: move;
| |
| }
| |
| canvas {
| |
| display: block;
| |
| }
| |
| </style>
| |
| </head>
| |
| <body>
| |
| <canvas id="canvas"></canvas>
| |
| <script>
| |
| const canvas = document.getElementById('canvas');
| |
| const ctx = canvas.getContext('2d');
| |
| | |
| const _0x5a1 = [70, 76, 65, 71, 123, 84, 119, 105, 110, 107, 108, 105, 110, 103, 95, 76, 105, 103, 104, 116, 115, 95, 65, 110, 100, 95, 66, 114, 105, 103, 104, 116, 95, 79, 114, 110, 97, 109, 101, 110, 116, 115, 125];
| |
| const hiddenPathChars = _0x5a1.map(c => String.fromCharCode(c));
| |
| | |
| let width, height;
| |
| let particles = [];
| |
| let snowParticles = [];
| |
|
| |
| let angleY = 0;
| |
| let targetAngleY = 0;
| |
| let isDragging = false;
| |
| let lastMouseX = 0;
| |
| let cameraZ = 550;
| |
| | |
| let mathDecorations = [
| |
| "x", "y", "z", "i", "e", "π", "ω", "t", "s", "f", "g",
| |
| "sin", "cos", "tan", "log", "ln", "lim", "det", "max", "min",
| |
| "f(t)", "F(ω)", "L(s)", "dy/dx", "x²+y²", "e^x", "sin(x)",
| |
| "ℱ{f}", "ℒ{f}", "∫e⁻ˢᵗ", "∫e⁻ⁱωᵗ",
| |
| "E=mc²", "F=ma", "V=IR", "A=πr²", "∇∙F", "∇×F",
| |
| "e^(iπ)+1=0", "sin²θ+cos²θ=1", "d/dx(e^x)=e^x",
| |
| "x=(-b±√D)/2a", "PV=nRT", "S=klogW"
| |
| ];
| |
| mathDecorations.sort((a, b) => a.length - b.length);
| |
| | |
| const treeGreens = ["#00FF00", "#32CD32", "#00FA9A", "#98FB98", "#7CFC00"];
| |
| const ornamentColors = ["#FF0000", "#FFD700", "#FF00FF", "#00FFFF"];
| |
| const lightColors = ["#FF5555", "#55FF55", "#5555FF", "#FFFF55", "#FF55FF", "#55FFFF"];
| |
| const snowSymbols = ["+", "-", "×", "÷", "=", "≠"];
| |
| | |
| class Particle {
| |
| constructor(x, y, z, text, color, type = 'leaf') {
| |
| this.x = x;
| |
| this.y = y;
| |
| this.z = z;
| |
| this.text = text;
| |
| this.color = color;
| |
| this.type = type;
| |
|
| |
| if (this.type === 'star') this.baseSize = 60;
| |
| else if (this.type === 'light') this.baseSize = 24;
| |
| else if (this.type === 'ornament') this.baseSize = 20;
| |
| else if (this.type === 'trunk') this.baseSize = 14;
| |
| else this.baseSize = 14;
| |
| | |
| this.projX = 0;
| |
| this.projY = 0;
| |
| this.projScale = 0;
| |
| this.visible = false;
| |
| this.depth = 0;
| |
| this.alpha = Math.random() * 0.5 + 0.5;
| |
| this.blinkPhase = Math.random() * Math.PI * 2;
| |
|
| |
| if (this.type === 'light') {
| |
| this.lightColor = lightColors[Math.floor(Math.random() * lightColors.length)];
| |
| }
| |
| }
| |
| project(cx, cy, angle) {
| |
| const cos = Math.cos(angle);
| |
| const sin = Math.sin(angle);
| |
| const rx = this.x * cos - this.z * sin;
| |
| const rz = this.x * sin + this.z * cos;
| |
| const focalLength = 450;
| |
| const scale = focalLength / (focalLength + rz + cameraZ);
| |
| this.projScale = scale;
| |
| this.projX = rx * scale + cx;
| |
| this.projY = this.y * scale + cy;
| |
| this.visible = (scale > 0 && rz > -cameraZ);
| |
| this.depth = rz;
| |
| }
| |
| draw(ctx) {
| |
| if (!this.visible) return;
| |
|
| |
| let alpha = this.alpha;
| |
| if (this.type === 'trunk') alpha = Math.max(alpha, 0.8);
| |
|
| |
| let drawAlpha = Math.max(0.1, Math.min(1, alpha * this.projScale));
| |
|
| |
| if(this.type === 'star' || this.type === 'light' || this.type === 'ornament') {
| |
| drawAlpha = 1;
| |
| }
| |
| | |
| ctx.globalAlpha = drawAlpha;
| |
| const fontSize = this.baseSize * this.projScale;
| |
| ctx.font = `bold ${fontSize}px 'Fira Code', monospace`;
| |
| | |
| if (this.type === 'star') {
| |
| ctx.shadowBlur = 40 + Math.abs(Math.sin(Date.now() * 0.002)) * 20;
| |
| ctx.shadowColor = "#FFD700";
| |
| ctx.fillStyle = "#FFFF00";
| |
| }
| |
| else if (this.type === 'light') {
| |
| const time = Date.now() * 0.005 + this.blinkPhase;
| |
| let twinkle = Math.pow(Math.sin(time) * 0.5 + 0.5, 4);
| |
|
| |
| ctx.globalAlpha = 0.4 + twinkle * 0.6;
| |
| ctx.shadowBlur = 10 + twinkle * 25;
| |
| ctx.shadowColor = this.lightColor;
| |
| ctx.fillStyle = "#FFFFFF";
| |
| }
| |
| else if (this.type === 'ornament') {
| |
| ctx.shadowBlur = 15;
| |
| ctx.shadowColor = this.color;
| |
| ctx.fillStyle = this.color;
| |
| }
| |
| else if (this.projScale > 0.6) {
| |
| ctx.shadowBlur = 5;
| |
| ctx.shadowColor = this.color;
| |
| ctx.fillStyle = this.color;
| |
| } else {
| |
| ctx.shadowBlur = 0;
| |
| ctx.fillStyle = this.color;
| |
| }
| |
| | |
| ctx.fillText(this.text, this.projX, this.projY);
| |
| ctx.globalAlpha = 1;
| |
| ctx.shadowBlur = 0;
| |
| }
| |
| }
| |
| | |
| class SnowParticle {
| |
| constructor() {
| |
| this.reset();
| |
| this.y = Math.random() * height - height / 2;
| |
| }
| |
| reset() {
| |
| this.x = (Math.random() - 0.5) * width * 1.5;
| |
| this.y = -height / 2 - Math.random() * 200;
| |
| this.z = (Math.random() - 0.5) * 1000;
| |
| this.text = snowSymbols[Math.floor(Math.random() * snowSymbols.length)];
| |
| this.color = "#FFFFFF";
| |
| this.speed = Math.random() * 2 + 1;
| |
| this.size = Math.random() * 10 + 8;
| |
| this.drift = Math.random() * 0.5 - 0.25;
| |
| }
| |
| update() {
| |
| this.y += this.speed;
| |
| this.x += this.drift;
| |
| if (this.y > height / 2 + 100) {
| |
| this.reset();
| |
| }
| |
| }
| |
| project(cx, cy, angle) {
| |
| const cos = Math.cos(angle * 0.1);
| |
| const sin = Math.sin(angle * 0.1);
| |
| const rx = this.x * cos - this.z * sin;
| |
| const rz = this.x * sin + this.z * cos;
| |
| const focalLength = 450;
| |
| const scale = focalLength / (focalLength + rz + cameraZ);
| |
| this.projScale = scale;
| |
| this.projX = rx * scale + cx;
| |
| this.projY = this.y * scale + cy;
| |
| this.visible = (scale > 0 && rz > -cameraZ);
| |
| }
| |
| draw(ctx) {
| |
| if (!this.visible) return;
| |
| ctx.globalAlpha = Math.min(1, this.projScale * 0.7);
| |
| ctx.font = `${this.size * this.projScale}px 'Fira Code', monospace`;
| |
| ctx.fillStyle = this.color;
| |
| if (this.projScale > 0.7) {
| |
| ctx.shadowBlur = 5;
| |
| ctx.shadowColor = "#FFFFFF";
| |
| }
| |
| ctx.fillText(this.text, this.projX, this.projY);
| |
| ctx.globalAlpha = 1;
| |
| ctx.shadowBlur = 0;
| |
| }
| |
| }
| |
| | |
| function createTree() {
| |
| particles = [];
| |
| const treeHeight = 650;
| |
| const maxRadius = 280;
| |
| const leafCount = 600;
| |
| const phi = Math.PI * (3 - Math.sqrt(5));
| |
| | |
| for (let i = 0; i < leafCount; i++) {
| |
| const y = - (treeHeight / 2) + (i / leafCount) * treeHeight - 80;
| |
| const radius = (i / leafCount) * maxRadius;
| |
| const theta = phi * i;
| |
| const x = Math.cos(theta) * radius;
| |
| const z = Math.sin(theta) * radius;
| |
| | |
| const progress = i / leafCount;
| |
| const centerIndex = progress * (mathDecorations.length - 1);
| |
| const noise = (Math.random() - 0.5) * 8;
| |
| let index = Math.floor(centerIndex + noise);
| |
| index = Math.max(0, Math.min(mathDecorations.length - 1, index));
| |
| const text = mathDecorations[index];
| |
|
| |
| let color, type;
| |
| if (Math.random() < 0.15) {
| |
| color = ornamentColors[Math.floor(Math.random() * ornamentColors.length)];
| |
| type = 'ornament';
| |
| } else {
| |
| color = treeGreens[Math.floor(Math.random() * treeGreens.length)];
| |
| type = 'leaf';
| |
| }
| |
| particles.push(new Particle(x, y, z, text, color, type));
| |
| }
| |
| | |
| const flagLen = hiddenPathChars.length;
| |
| for (let i = 0; i < flagLen; i++) {
| |
| const y = - (treeHeight / 2) + (i / flagLen) * (treeHeight * 0.8) - 60;
| |
| const radius = ((i + 2) / (flagLen + 5)) * maxRadius * 0.95;
| |
| const theta = (i * 1.5) + 0;
| |
| const x = Math.cos(theta) * radius;
| |
| const z = Math.sin(theta) * radius;
| |
| particles.push(new Particle(x, y, z, hiddenPathChars[i], "#FFFFFF", 'light'));
| |
| }
| |
| | |
| const trunkH = 160;
| |
| const trunkW = 70;
| |
| const trunkCount = 120;
| |
| const trunkStartY = (treeHeight / 2) - 60;
| |
| | |
| for(let i=0; i<trunkCount; i++) {
| |
| const h = Math.random() * trunkH;
| |
| const angle = Math.random() * Math.PI * 2;
| |
| const r = trunkW;
| |
| const x = Math.cos(angle) * r;
| |
| const z = Math.sin(angle) * r;
| |
| const y = trunkStartY + h;
| |
| const text = Math.random() > 0.5 ? "1" : "0";
| |
| particles.push(new Particle(x, y, z, text, "#D2691E", 'trunk'));
| |
| }
| |
|
| |
| particles.push(new Particle(0, -treeHeight/2 - 120, 0, "★", "#FFFF00", 'star'));
| |
| }
| |
| | |
| function createSnow() {
| |
| snowParticles = [];
| |
| const snowCount = 200;
| |
| for (let i = 0; i < snowCount; i++) {
| |
| snowParticles.push(new SnowParticle());
| |
| }
| |
| }
| |
| | |
| function resize() {
| |
| width = canvas.width = window.innerWidth;
| |
| height = canvas.height = window.innerHeight;
| |
| }
| |
| | |
| function animate() {
| |
| ctx.fillStyle = "#020205";
| |
| ctx.fillRect(0, 0, width, height);
| |
| if (!isDragging) targetAngleY += 0.002;
| |
| angleY += (targetAngleY - angleY) * 0.1;
| |
| | |
| snowParticles.forEach(p => {
| |
| p.update();
| |
| p.project(width / 2, height / 2, angleY);
| |
| p.draw(ctx);
| |
| });
| |
| | |
| particles.forEach(p => p.project(width / 2, height / 2, angleY));
| |
| particles.sort((a, b) => b.depth - a.depth);
| |
| ctx.textAlign = 'center';
| |
| ctx.textBaseline = 'middle';
| |
| particles.forEach(p => p.draw(ctx));
| |
|
| |
| requestAnimationFrame(animate);
| |
| }
| |
| | |
| window.addEventListener('resize', resize);
| |
| window.addEventListener('wheel', (e) => {
| |
| cameraZ += e.deltaY * 0.5;
| |
| cameraZ = Math.max(200, Math.min(1000, cameraZ));
| |
| });
| |
| | |
| const onStart = (x) => { isDragging = true; lastMouseX = x; };
| |
| const onMove = (x) => {
| |
| if (!isDragging) return;
| |
| const delta = (x - lastMouseX) * 0.005;
| |
| targetAngleY += delta;
| |
| angleY += delta;
| |
| lastMouseX = x;
| |
| };
| |
| const onEnd = () => { isDragging = false; };
| |
| | |
| canvas.addEventListener('mousedown', e => onStart(e.clientX));
| |
| window.addEventListener('mousemove', e => onMove(e.clientX));
| |
| window.addEventListener('mouseup', onEnd);
| |
| canvas.addEventListener('touchstart', e => onStart(e.touches[0].clientX));
| |
| window.addEventListener('touchmove', e => onMove(e.touches[0].clientX));
| |
| window.addEventListener('touchend', onEnd);
| |
| | |
| resize();
| |
| createTree();
| |
| createSnow();
| |
| animate();
| |
| </script>
| |
| </body>
| |
| </html>
| |