Полный перенос дизайна и текстов из макета: oklch-палитра, Google Fonts
Заменил приблизительную 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), а не по сырым числам.
This commit is contained in:
+24
-24
@@ -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);
|
||||
}
|
||||
|
||||
+7
-12
@@ -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) => `
|
||||
<li class="project-card">
|
||||
<div class="project-card__bar"><span></span><span class="project-card__perms">drwxr-xr-x</span> ${p.name}</div>
|
||||
<div class="project-card__body">
|
||||
<h3 class="project-card__title">${p.title}</h3>
|
||||
<p class="project-card__desc">${p.description}</p>
|
||||
<ul class="tag-list">
|
||||
${p.tags.map((tag) => `<li class="tag">${tag}</li>`).join('')}
|
||||
</ul>
|
||||
<a class="project-card__link" href="${p.url}" target="_blank" rel="noopener">${p.linkLabel}: ${p.url.replace('https://', '')}</a>
|
||||
<div class="project-card__header">
|
||||
<div class="project-card__name"><span class="project-card__perms">drwxr-xr-x</span> ${p.name}</div>
|
||||
<a href="${p.url}" target="_blank" rel="noopener">${p.linkLabel} →</a>
|
||||
</div>
|
||||
<p class="project-card__desc">${p.description}</p>
|
||||
<ul class="tag-list">
|
||||
${p.tags.map((tag) => `<li class="tag">${tag}</li>`).join('')}
|
||||
</ul>
|
||||
</li>`
|
||||
)
|
||||
.join('');
|
||||
|
||||
+7
-11
@@ -178,9 +178,10 @@ function renderExperience(list) {
|
||||
.map(
|
||||
(job) => `
|
||||
<li class="timeline-item">
|
||||
<div class="timeline-item__meta">${job.period} · ${job.duration} · ${job.location}</div>
|
||||
<h3 class="timeline-item__role">${job.role} — ${job.company}</h3>
|
||||
<ul>${job.bullets.map((b) => `<li>${b}</li>`).join('')}</ul>
|
||||
<div class="timeline-item__meta">${job.period} · ${job.duration}</div>
|
||||
<h3 class="timeline-item__role">${job.role}</h3>
|
||||
<div class="timeline-item__company">${job.company} · ${job.location}</div>
|
||||
<ul>${job.bullets.map((b) => `<li><span class="dash">-</span><span>${b}</span></li>`).join('')}</ul>
|
||||
</li>`
|
||||
)
|
||||
.join('');
|
||||
@@ -229,12 +230,7 @@ function render() {
|
||||
|
||||
<section class="about">
|
||||
<h2 class="section-heading"><span class="section-heading__num">01</span>${t.aboutHeading}</h2>
|
||||
<div class="term-window">
|
||||
<div class="term-window__bar"><span></span><span></span><span></span></div>
|
||||
<div class="term-window__body">
|
||||
${t.about.map((p) => `<p>${p}</p>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
${t.about.map((p) => `<p>${p}</p>`).join('')}
|
||||
</section>
|
||||
|
||||
<section class="experience">
|
||||
@@ -255,8 +251,8 @@ function render() {
|
||||
<section class="contact">
|
||||
<h2 class="section-heading"><span class="section-heading__num">05</span>${t.contactHeading}</h2>
|
||||
<ul class="contact-list">
|
||||
<li><a href="mailto:${t.contact.email}">${t.contact.email}</a></li>
|
||||
<li><a href="${t.contact.telegramUrl}">${t.contact.telegram}</a></li>
|
||||
<li><span class="contact-list__label">email</span><a href="mailto:${t.contact.email}">${t.contact.email}</a></li>
|
||||
<li><span class="contact-list__label">telegram</span><a href="${t.contact.telegramUrl}">${t.contact.telegram}</a></li>
|
||||
</ul>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user