From 556f5761fdfca75afbf0c6549f496c64a4b8549e Mon Sep 17 00:00:00 2001 From: Dmitry Gammel Date: Mon, 10 Aug 2026 00:39:53 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=BB=D0=BD=D1=8B=D0=B9=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=BD=D0=BE=D1=81=20=D0=B4=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D0=B9=D0=BD=D0=B0=20=D0=B8=20=D1=82=D0=B5=D0=BA=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=B8=D0=B7=20=D0=BC=D0=B0=D0=BA=D0=B5=D1=82?= =?UTF-8?q?=D0=B0:=20oklch-=D0=BF=D0=B0=D0=BB=D0=B8=D1=82=D1=80=D0=B0,=20G?= =?UTF-8?q?oogle=20Fonts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Заменил приблизительную hex-палитру на настоящие oklch()-цвета из макета, подключил JetBrains Mono через Google Fonts (обновил CSP: style-src/font-src для fonts.googleapis.com/fonts.gstatic.com). Секция "обо мне" резюме — без term-window, плоские абзацы; опыт работы в формате период/роль/компания·город + буллеты с тире; контакты в виде label+value, как в макете. Карточки проектов — без бара с точками, header-строка "drwxr-xr-x {name}" + ссылка справа, как в Projects.dc.html. matrix-rain.js переписан по алгоритму из макета (shadowBlur на "голове" капли, katakana-набор символов, накопительное падение). Градиент глубины поверх дождя взял по факту темнее, чем в инлайн-стилях макета (0.72–0.96 давал почти невидимый дождь) — подобрал по референс-скриншоту макета (0.35–0.6), а не по сырым числам. --- frontend/css/projects.css | 70 ++++++++++---------------------- frontend/css/resume.css | 74 ++++++++++++++++++++-------------- frontend/css/theme.css | 82 +++++++++++++++++--------------------- frontend/game.html | 3 ++ frontend/index.html | 3 ++ frontend/js/matrix-rain.js | 48 +++++++++++----------- frontend/js/projects.js | 19 ++++----- frontend/js/resume.js | 18 ++++----- frontend/projects.html | 3 ++ nginx/nginx.conf | 2 +- 10 files changed, 148 insertions(+), 174 deletions(-) diff --git a/frontend/css/projects.css b/frontend/css/projects.css index dfc966d..2792e40 100644 --- a/frontend/css/projects.css +++ b/frontend/css/projects.css @@ -3,81 +3,51 @@ font-weight: 700; color: var(--color-accent); margin: 0 0 0.4rem; - text-shadow: 0 0 20px var(--color-accent-dim); + text-shadow: 0 0 24px oklch(55% 0.19 142 / 0.4); } .page-subheading { color: var(--color-silver); font-size: 0.9rem; - margin: 0 0 2rem; + margin: 0 0 2.5rem; } .project-grid { list-style: none; margin: 0; padding: 0; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 1.5rem; + display: flex; + flex-direction: column; + gap: 1.25rem; } .project-card { border: var(--border); - background: var(--color-bg-raised); - display: flex; - flex-direction: column; + padding: 1.5rem; } -.project-card__bar { +.project-card__header { display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 0.75rem; - border-bottom: var(--border); - color: var(--color-silver); - font-size: 0.8rem; - white-space: nowrap; - overflow-x: auto; + align-items: baseline; + justify-content: space-between; + gap: 0.75rem; + flex-wrap: wrap; + margin-bottom: 0.6rem; } -.project-card__bar span { - width: 9px; - height: 9px; - border-radius: 50%; - background: var(--color-accent-dim); - opacity: 0.6; - flex-shrink: 0; +.project-card__name { + font-size: 1.05rem; + font-weight: 600; + color: var(--color-fg-dim); } .project-card__perms { - color: var(--color-accent-dim); -} - -.project-card__body { - padding: 1.25rem 1.5rem; - display: flex; - flex-direction: column; - gap: 0.75rem; - flex: 1; -} - -.project-card__title { - margin: 0; - color: var(--color-accent); - font-size: 1.05rem; + color: var(--color-accent-mid); } .project-card__desc { - margin: 0; - color: var(--color-fg); + margin: 0 0 0.75rem; + color: var(--color-fg-dim); font-size: 0.9rem; -} - -.project-card__link { - margin-top: auto; -} - -.project-card__link::before { - content: '$ '; - color: var(--color-accent-dim); + line-height: 1.6; } diff --git a/frontend/css/resume.css b/frontend/css/resume.css index 7e9b0f4..53de471 100644 --- a/frontend/css/resume.css +++ b/frontend/css/resume.css @@ -18,7 +18,7 @@ .hero-text .tagline { margin: 0.75rem 0 0; - color: var(--color-accent-dim); + color: var(--color-accent-mid); max-width: 34ch; } @@ -29,7 +29,7 @@ font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase; - color: var(--color-accent-dim); + color: var(--color-accent-mid); margin: 0 0 0.75rem; } @@ -38,7 +38,7 @@ height: 7px; border-radius: 50%; background: var(--color-accent); - box-shadow: 0 0 8px var(--color-accent-dim); + box-shadow: 0 0 10px var(--color-accent); animation: blink 1.6s step-end infinite; } @@ -48,49 +48,62 @@ section { .about p { max-width: 68ch; + font-size: 0.94rem; + line-height: 1.7; + color: var(--color-fg); + margin: 0 0 1rem; } .timeline { - border-left: var(--border); margin: 0; - padding: 0 0 0 1.5rem; + padding: 0; list-style: none; } .timeline-item { - position: relative; - margin-bottom: 2rem; -} - -.timeline-item::before { - content: ''; - position: absolute; - left: -1.6rem; - top: 0.4rem; - width: 8px; - height: 8px; - background: var(--color-accent); - box-shadow: 0 0 6px var(--color-accent-dim); + border-left: var(--border); + padding-left: 1rem; + margin-bottom: 1.75rem; } .timeline-item__meta { - color: var(--color-silver); - font-size: 0.85rem; + color: var(--color-silver-dim); + font-size: 0.78rem; + letter-spacing: 0.02em; + margin-bottom: 0.2rem; } .timeline-item__role { - color: var(--color-accent); - margin: 0.15rem 0 0.5rem; - font-size: 1rem; + color: var(--color-fg); + font-weight: 600; + margin: 0 0 0.15rem; + font-size: 0.94rem; +} + +.timeline-item__company { + color: var(--color-silver); + font-size: 0.82rem; + margin-bottom: 0.6rem; } .timeline-item ul { margin: 0; - padding-left: 1.2rem; + padding: 0; + list-style: none; } -.timeline-item li::marker { - color: var(--color-accent-dim); +.timeline-item li { + display: flex; + gap: 0.5rem; + font-size: 0.88rem; + line-height: 1.6; + color: var(--color-fg-dim); + margin-bottom: 0.35rem; +} + +.timeline-item .dash { + color: var(--color-accent-mid); + flex-shrink: 0; } .contact-list { @@ -99,12 +112,13 @@ section { padding: 0; display: flex; flex-direction: column; - gap: 0.4rem; + gap: 0.5rem; + font-size: 0.9rem; } -.contact-list a::before { - content: '$ '; - color: var(--color-accent-dim); +.contact-list__label { + color: var(--color-silver-dim); + margin-right: 0.5rem; } .skill-group { diff --git a/frontend/css/theme.css b/frontend/css/theme.css index 77bd6cb..abe4bf9 100644 --- a/frontend/css/theme.css +++ b/frontend/css/theme.css @@ -1,12 +1,17 @@ :root { - --color-bg: #0a0e0c; - --color-bg-raised: #0f1512; - --color-fg: #c7c7c7; - --color-accent: #00ff41; - --color-accent-dim: #0a8f2c; - --color-silver: #9fa3a0; - --font-mono: 'JetBrains Mono', 'Share Tech Mono', ui-monospace, monospace; - --border: 1px solid #163020; + --color-bg: #050705; + --color-bg-raised: #0d100d; + --color-fg: oklch(84% 0.01 240); + --color-fg-dim: oklch(76% 0.01 240); + --color-accent: oklch(80% 0.19 142); + --color-accent-bright: oklch(88% 0.17 142); + --color-accent-mid: oklch(55% 0.13 142); + --color-accent-dim: oklch(50% 0.13 142); + --color-silver: oklch(58% 0.01 240); + --color-silver-dim: oklch(48% 0.01 240); + --color-num: oklch(40% 0.02 240); + --font-mono: 'JetBrains Mono', ui-monospace, monospace; + --border: 1px solid oklch(32% 0.06 142 / 0.45); } * { @@ -25,9 +30,19 @@ body { line-height: 1.5; } +/* лёгкий градиент глубины поверх дождя, под контентом */ +body::after { + content: ''; + position: fixed; + inset: 0; + z-index: -1; + pointer-events: none; + background: linear-gradient(180deg, rgba(4, 6, 4, 0.35), rgba(4, 6, 4, 0.55) 220px, rgba(4, 6, 4, 0.6)); +} + ::selection { - background: var(--color-accent-dim); - color: var(--color-bg); + background: oklch(42% 0.14 142); + color: #eafff0; } a { @@ -67,11 +82,10 @@ body::before { z-index: -1; pointer-events: none; background: repeating-linear-gradient( - to bottom, - rgba(0, 0, 0, 0.15) 0px, - rgba(0, 0, 0, 0.15) 1px, - transparent 1px, - transparent 3px + 180deg, + rgba(0, 0, 0, 0) 0px, + rgba(0, 0, 0, 0) 2px, + rgba(80, 255, 140, 0.025) 3px ); } @@ -81,7 +95,7 @@ body::before { top: 0; z-index: 10; border-bottom: var(--border); - background: rgba(10, 14, 12, 0.6); + background: rgba(4, 6, 4, 0.55); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); } @@ -162,32 +176,6 @@ body::before { color: var(--color-accent-dim); } -/* --- терминальное окно-обёртка для блоков контента --- */ -.term-window { - border: var(--border); - background: var(--color-bg-raised); - margin: 1.75rem 0; -} - -.term-window__bar { - display: flex; - gap: 0.4rem; - padding: 0.5rem 0.75rem; - border-bottom: var(--border); -} - -.term-window__bar span { - width: 9px; - height: 9px; - border-radius: 50%; - background: var(--color-accent-dim); - opacity: 0.6; -} - -.term-window__body { - padding: 1.25rem 1.5rem; -} - /* --- ASCII-арт --- */ .ascii-cactus { color: var(--color-accent); @@ -252,15 +240,17 @@ body::before { display: flex; align-items: baseline; gap: 0.75rem; - font-size: 1rem; - font-weight: 400; + font-size: 0.9rem; + font-weight: 500; + letter-spacing: 0.03em; + text-transform: uppercase; border-bottom: var(--border); padding-bottom: 0.6rem; margin-bottom: 1.5rem; - color: var(--color-fg); + color: var(--color-accent-mid); } .section-heading__num { - color: var(--color-accent-dim); + color: var(--color-num); font-size: 0.85rem; } diff --git a/frontend/game.html b/frontend/game.html index fff481f..6836907 100644 --- a/frontend/game.html +++ b/frontend/game.html @@ -5,6 +5,9 @@ ./battlecity — cacto + + + diff --git a/frontend/index.html b/frontend/index.html index e237f4b..9509451 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,6 +6,9 @@ cacto — Гаммель Дмитрий, PHP/Go backend + + + diff --git a/frontend/js/matrix-rain.js b/frontend/js/matrix-rain.js index dbfc4f3..9271907 100644 --- a/frontend/js/matrix-rain.js +++ b/frontend/js/matrix-rain.js @@ -2,47 +2,47 @@ const canvas = document.getElementById('matrix-rain'); if (canvas && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) { const ctx = canvas.getContext('2d'); - const chars = 'アカサタナハマヤラワ0123456789ABCDEFcacto$#@%&*'.split(''); - const fontSize = 15; - const bg = getComputedStyle(document.documentElement).getPropertyValue('--color-bg').trim(); + const chars = 'アカサタナハマヤラワ01アイウエオカキク'; + const fontSize = 16; let columns = 0; let drops = []; + let frameCount = 0; - function setup() { + function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; columns = Math.floor(canvas.width / fontSize); - drops = new Array(columns).fill(0).map(() => Math.floor((Math.random() * canvas.height) / fontSize)); + drops = new Array(columns).fill(0).map(() => Math.random() * -50); } function draw() { - ctx.fillStyle = `${bg}cc`; + ctx.fillStyle = 'rgba(3, 5, 3, 0.12)'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.font = `${fontSize}px monospace`; - for (let i = 0; i < columns; i++) { - const char = chars[Math.floor(Math.random() * chars.length)]; - const x = i * fontSize; - const y = drops[i] * fontSize; - - // «голова» капли — редкая, чуть ярче, со свечением - const isHead = Math.random() > 0.94; - ctx.shadowColor = 'rgba(100, 255, 160, 0.9)'; - ctx.shadowBlur = isHead ? 10 : 0; - ctx.fillStyle = isHead ? '#d2ffe1' : Math.random() > 0.98 ? '#9fa3a0' : '#00ff41'; - ctx.fillText(char, x, y); - - if (y > canvas.height && Math.random() > 0.975) { + for (let i = 0; i < drops.length; i++) { + const text = chars[Math.floor(Math.random() * chars.length)]; + const isHead = Math.random() > 0.92; + ctx.shadowColor = 'rgba(100, 255, 160, 1)'; + ctx.shadowBlur = isHead ? 12 : 2; + ctx.fillStyle = isHead ? 'rgba(210, 255, 225, 1)' : 'rgba(80, 255, 140, 1)'; + ctx.fillText(text, i * fontSize, drops[i] * fontSize); + if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) { drops[i] = 0; - } else { - drops[i]++; } + drops[i] += 0.4; } ctx.shadowBlur = 0; } - setup(); - window.addEventListener('resize', setup); - setInterval(draw, 50); + function tick() { + frameCount++; + if (frameCount % 2 === 0) draw(); + requestAnimationFrame(tick); + } + + resize(); + window.addEventListener('resize', resize); + requestAnimationFrame(tick); } diff --git a/frontend/js/projects.js b/frontend/js/projects.js index 459d67c..85505c3 100644 --- a/frontend/js/projects.js +++ b/frontend/js/projects.js @@ -7,7 +7,6 @@ const DATA = { projects: [ { name: 'home_automatization', - title: 'Home Automation Platform', description: 'Микросервисная платформа умного дома на Go и Laravel. Первая зона — гроубокс (полив, свет, климат), дальше — весь дом.', tags: ['Go', 'Laravel', 'Docker', 'RabbitMQ', 'ClickHouse', 'gRPC', 'MQTT'], @@ -16,7 +15,6 @@ const DATA = { }, { name: 'cactoz.su', - title: 'cactoz.su — этот сайт', description: 'Личный сайт-визитка: резюме, проекты и мультиплеерный клон BattleCity. Тема — Матрица.', tags: ['HTML/CSS/JS', 'C++', 'uWebSockets', 'Quintus.js', 'nginx', 'Docker'], @@ -31,7 +29,6 @@ const DATA = { projects: [ { name: 'home_automatization', - title: 'Home Automation Platform', description: 'A microservice smart-home platform on Go and Laravel. First zone is a grow box (watering, lighting, climate), the whole house comes next.', tags: ['Go', 'Laravel', 'Docker', 'RabbitMQ', 'ClickHouse', 'gRPC', 'MQTT'], @@ -40,7 +37,6 @@ const DATA = { }, { name: 'cactoz.su', - title: 'cactoz.su — this site', description: 'A personal portfolio site: résumé, projects and a multiplayer BattleCity clone. Matrix theme throughout.', tags: ['HTML/CSS/JS', 'C++', 'uWebSockets', 'Quintus.js', 'nginx', 'Docker'], @@ -66,15 +62,14 @@ function render() { .map( (p) => `
  • -
    drwxr-xr-x ${p.name}
    -
    -

    ${p.title}

    -

    ${p.description}

    -
      - ${p.tags.map((tag) => `
    • ${tag}
    • `).join('')} -
    - ${p.linkLabel}: ${p.url.replace('https://', '')} +
    +
    drwxr-xr-x ${p.name}
    + ${p.linkLabel} →
    +

    ${p.description}

    +
      + ${p.tags.map((tag) => `
    • ${tag}
    • `).join('')} +
  • ` ) .join(''); diff --git a/frontend/js/resume.js b/frontend/js/resume.js index 5c9d45c..37ed0df 100644 --- a/frontend/js/resume.js +++ b/frontend/js/resume.js @@ -178,9 +178,10 @@ function renderExperience(list) { .map( (job) => `
  • -
    ${job.period} · ${job.duration} · ${job.location}
    -

    ${job.role} — ${job.company}

    - +
    ${job.period} · ${job.duration}
    +

    ${job.role}

    +
    ${job.company} · ${job.location}
    +
  • ` ) .join(''); @@ -229,12 +230,7 @@ function render() {

    01${t.aboutHeading}

    -
    -
    -
    - ${t.about.map((p) => `

    ${p}

    `).join('')} -
    -
    + ${t.about.map((p) => `

    ${p}

    `).join('')}
    @@ -255,8 +251,8 @@ function render() {

    05${t.contactHeading}

    `; } diff --git a/frontend/projects.html b/frontend/projects.html index 07c071a..97d485c 100644 --- a/frontend/projects.html +++ b/frontend/projects.html @@ -6,6 +6,9 @@ ls projects/ — cacto + + + diff --git a/nginx/nginx.conf b/nginx/nginx.conf index de476fd..fbf09ef 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -13,7 +13,7 @@ server { add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options DENY always; add_header Referrer-Policy strict-origin-when-cross-origin always; - add_header Content-Security-Policy "default-src 'self'; connect-src 'self' wss://cactoz.su wss://www.cactoz.su; img-src 'self' data:; style-src 'self' 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'" always; + add_header Content-Security-Policy "default-src 'self'; connect-src 'self' wss://cactoz.su wss://www.cactoz.su; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; base-uri 'none'; frame-ancestors 'none'" always; location / { try_files $uri $uri.html $uri/ =404;