<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="https://androman.pro/wp-content/themes/androman/assets/feed.xsl?ver=0.9.4" type="text/xsl"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>npm &#8212; ANDROMAN.PRO</title>
	<atom:link href="https://androman.pro/tag/npm/feed/" rel="self" type="application/rss+xml" />
	<link>https://androman.pro</link>
	<description>Независимый 1С-архитектор и AI-автоматизация корпоративных процессов: аудит, PoC на реальных данных, проверка качества и внедрение. · build 0.9.4</description>
	<lastBuildDate>Tue, 05 May 2026 11:57:48 +0000</lastBuildDate>
	<language>ru-RU</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
	<item>
		<title>Продуктивная прокрастинация</title>
		<link>https://androman.pro/2026/04/30/cyberpunk-captcha/</link>
					<comments>https://androman.pro/2026/04/30/cyberpunk-captcha/#respond</comments>
		
		<dc:creator><![CDATA[Androman.pro]]></dc:creator>
		<pubDate>Wed, 29 Apr 2026 21:00:00 +0000</pubDate>
				<category><![CDATA[дев-лог]]></category>
		<category><![CDATA[лаборатория]]></category>
		<category><![CDATA[anti-spam]]></category>
		<category><![CDATA[captcha]]></category>
		<category><![CDATA[cyberpunk]]></category>
		<category><![CDATA[npm]]></category>
		<category><![CDATA[vanilla-js]]></category>
		<guid isPermaLink="false">https://androman.pro/2026/04/30/cyberpunk-captcha/</guid>

					<description><![CDATA[Шесть кибербанк-капч за час продуктивной прокрастинации — декодер, лава-шар, sequence, honeypot, поиск бага в 1С-коде, scrub-reveal. Все живые прямо в посте.]]></description>
										<content:encoded><![CDATA[<style>
/* ─── cyberpunk-captcha shared styles ─── */
/* cyberpunk-captcha — shared styles.
 * Design language: cyan + magenta accents, monospace mono labels, glow shadows.
 * All colors via CSS custom properties — override at .cpcap level for theming.
 */

.cpcap {
  --cpcap-cyan: #00d4ff;
  --cpcap-magenta: #ff40c0;
  --cpcap-amber: #ffaa20;
  --cpcap-signal-green: #00ff88;
  --cpcap-hot: #ff4060;
  --cpcap-bg: #0a0e1a;
  --cpcap-bg-alt: #131825;
  --cpcap-text: #e6edf3;
  --cpcap-text-dim: #b5bcc4;
  --cpcap-text-muted: #6f7686;
  --cpcap-border: #2a3142;
  --cpcap-ease: cubic-bezier(0.22, 1, 0.36, 1);

  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: var(--cpcap-bg-alt);
  border: 1px solid rgba(0, 212, 255, 0.18);
  border-radius: 4px;
  padding: 22px 18px 18px;
  color: var(--cpcap-text);
  margin: 16px 0;
  position: relative;
  box-shadow: 0 0 0 1px rgba(0, 212, 255, 0.08), inset 0 0 36px rgba(0, 212, 255, 0.025);
}
/* Corner brackets — 4 L-shapes на углах (8 background layers форм. 4 пары линий) */
.cpcap::before {
  content: '';
  position: absolute;
  inset: 4px;
  pointer-events: none;
  background:
    linear-gradient(to right, var(--cpcap-cyan), var(--cpcap-cyan)) top    left  / 14px 1px no-repeat,
    linear-gradient(to bottom, var(--cpcap-cyan), var(--cpcap-cyan)) top    left  / 1px 14px no-repeat,
    linear-gradient(to right, var(--cpcap-cyan), var(--cpcap-cyan)) top    right / 14px 1px no-repeat,
    linear-gradient(to bottom, var(--cpcap-cyan), var(--cpcap-cyan)) top    right / 1px 14px no-repeat,
    linear-gradient(to right, var(--cpcap-cyan), var(--cpcap-cyan)) bottom left  / 14px 1px no-repeat,
    linear-gradient(to bottom, var(--cpcap-cyan), var(--cpcap-cyan)) bottom left  / 1px 14px no-repeat,
    linear-gradient(to right, var(--cpcap-cyan), var(--cpcap-cyan)) bottom right / 14px 1px no-repeat,
    linear-gradient(to bottom, var(--cpcap-cyan), var(--cpcap-cyan)) bottom right / 1px 14px no-repeat;
  opacity: 0.6;
  z-index: 1;
}
/* Subtle scanlines overlay на фоне всей капчи */
.cpcap::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    0deg,
    rgba(0, 212, 255, 0.025) 0px,
    rgba(0, 212, 255, 0.025) 1px,
    transparent 1px,
    transparent 4px
  );
  border-radius: inherit;
  z-index: 0;
  mix-blend-mode: screen;
}
.cpcap > * { position: relative; z-index: 2; }

.cpcap-label {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-family: 'JetBrains Mono', 'Courier New', monospace;
  font-size: 11px;
  color: var(--cpcap-cyan);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  margin-bottom: 14px;
  padding-bottom: 8px;
  border-bottom: 1px dashed rgba(0, 212, 255, 0.15);
}
.cpcap-label > span:first-child {
  text-shadow: 0 0 6px rgba(0, 212, 255, 0.35);
}
/* Pulsing dot before label first span */
.cpcap-label > span:first-child::before {
  content: '●';
  display: inline-block;
  margin-right: 6px;
  color: var(--cpcap-signal-green);
  text-shadow: 0 0 6px var(--cpcap-signal-green);
  animation: cpcapPulse 1.6s ease-in-out infinite;
  vertical-align: middle;
  font-size: 10px;
}
@keyframes cpcapPulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: 0.5; transform: scale(1.3); }
}

.cpcap-status {
  text-align: center;
  margin-top: 10px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  letter-spacing: 0.1em;
  color: var(--cpcap-text-muted);
  min-height: 18px;
}
.cpcap-status.cpcap-ok    { color: var(--cpcap-cyan); }
.cpcap-status.cpcap-warn  { color: var(--cpcap-amber); }
.cpcap-status.cpcap-error { color: var(--cpcap-hot); }

.cpcap-input {
  display: block;
  width: 100%;
  background: var(--cpcap-bg);
  color: var(--cpcap-text);
  border: 1px solid var(--cpcap-border);
  border-radius: 4px;
  padding: 8px 12px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 14px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  box-sizing: border-box;
  outline: none;
  transition: border-color 0.2s, box-shadow 0.2s;
}
.cpcap-input:focus {
  border-color: var(--cpcap-cyan);
  box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.08);
}

.cpcap-button {
  background: transparent;
  border: 1px solid var(--cpcap-cyan);
  color: var(--cpcap-cyan);
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  padding: 6px 14px;
  border-radius: 4px;
  cursor: pointer;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  transition: all 0.15s;
}
.cpcap-button:hover:not(:disabled) {
  background: rgba(0, 212, 255, 0.1);
  box-shadow: 0 0 12px rgba(0, 212, 255, 0.4);
}

/* ── DecoderLock ── */
.cpcap-decoder-display {
  font-family: 'JetBrains Mono', monospace;
  font-size: 24px;
  letter-spacing: 0.12em;
  text-align: center;
  padding: 18px 12px;
  min-height: 32px;
  background:
    radial-gradient(ellipse at center, rgba(0, 212, 255, 0.06), transparent 70%),
    linear-gradient(180deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.4)),
    var(--cpcap-bg);
  border: 1px solid rgba(0, 212, 255, 0.2);
  border-radius: 4px;
  margin: 6px 0 12px;
  position: relative;
  overflow: hidden;
  text-shadow: 0 0 8px rgba(0, 212, 255, 0.35);
  color: var(--cpcap-text);
}
.cpcap-decoder-display::before {
  content: '';
  position: absolute;
  left: 0; right: 0; height: 2px;
  background: linear-gradient(90deg, transparent, var(--cpcap-cyan), transparent);
  box-shadow: 0 0 12px var(--cpcap-cyan);
  animation: cpcapDecoderBeam 2.2s linear infinite;
  pointer-events: none;
  top: 0;
}
@keyframes cpcapDecoderBeam {
  0%   { top: -2px; opacity: 0.8; }
  90%  { opacity: 0.8; }
  100% { top: 100%; opacity: 0; }
}

/* ── LavaOrbTemp ── */
.cpcap-lava-target {
  padding: 2px 8px;
  border: 1px solid currentColor;
  border-radius: 3px;
  background: rgba(0, 212, 255, 0.08);
  text-shadow: 0 0 8px rgba(0, 212, 255, 0.5);
  font-family: 'JetBrains Mono', monospace;
  font-size: 10px;
}
.cpcap-lava-wrap {
  padding: 70px 20px 90px;
  display: block;
  position: relative;
}
.cpcap-lava-wrap::before {
  content: '';
  position: absolute;
  top: 50%; left: 6px; right: 6px;
  height: 26px;
  transform: translateY(-50%);
  background: repeating-linear-gradient(
    90deg,
    rgba(0, 212, 255, 0.15) 0px,
    rgba(0, 212, 255, 0.15) 1px,
    transparent 1px,
    transparent 10%
  );
  pointer-events: none;
  opacity: 0.5;
}
.cpcap-lava-wrap input[type=range] {
  width: 100%;
  display: block;
  margin: 0;
  -webkit-appearance: none;
  appearance: none;
  height: 8px;
  background: linear-gradient(90deg, #00d4ff 0%, #6699ff 20%, #ffaa20 50%, #ff6633 75%, #ff2040 100%);
  border-radius: 4px;
  box-shadow: 0 0 0 1px rgba(0, 212, 255, 0.3), 0 0 14px rgba(255, 64, 96, 0.25);
  outline: none;
  cursor: grab;
}
.cpcap-lava-wrap input[type=range]:active { cursor: grabbing; }
.cpcap-lava-wrap input[type=range]:focus {
  box-shadow: 0 0 0 2px var(--cpcap-cyan), 0 0 20px rgba(0, 212, 255, 0.4);
}
.cpcap-lava-wrap input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 60px;
  height: 60px;
  background: transparent;
  border: 0;
  cursor: grab;
  opacity: 0;
}
.cpcap-lava-wrap input[type=range]::-moz-range-thumb {
  width: 60px;
  height: 60px;
  background: transparent;
  border: 0;
  cursor: grab;
  opacity: 0;
}
.cpcap-lava-wrap input[type=range]::-moz-range-track {
  height: 8px;
  background: transparent;
  border: 0;
}
.cpcap-lava-bands {
  display: grid;
  grid-template-columns: 16% 19% 19% 19% 27%;
  gap: 0;
  margin-top: 4px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 9px;
  letter-spacing: 0.1em;
  color: var(--cpcap-text-muted);
  text-align: center;
}
.cpcap-lava-bands span { padding: 2px 0; border-top: 1px dashed rgba(0, 212, 255, 0.15); }
.cpcap-lava-readout {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 12px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  letter-spacing: 0.1em;
}
.cpcap-lava-readout .side { color: var(--cpcap-text-muted); }
.cpcap-lava-readout .current {
  color: var(--cpcap-cyan);
  font-weight: 700;
  font-size: 14px;
  padding: 4px 12px;
  border: 1px solid var(--cpcap-cyan);
  border-radius: 3px;
  background: rgba(0, 212, 255, 0.08);
  text-shadow: 0 0 8px rgba(0, 212, 255, 0.5);
}

/* ── SequenceDecode ── */
.cpcap-seq-pad {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
  max-width: 320px;
  margin: 12px auto;
}
.cpcap-seq-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  aspect-ratio: 1.6 / 1;
  background:
    linear-gradient(135deg, rgba(0, 212, 255, 0.04), rgba(0, 212, 255, 0)),
    var(--cpcap-bg);
  border: 1px solid var(--cpcap-border);
  border-radius: 6px;
  cursor: pointer;
  transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s, background 0.15s;
  color: var(--cpcap-text);
  position: relative;
  overflow: hidden;
}
.cpcap-seq-btn::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, currentColor, transparent);
  opacity: 0.4;
}
.cpcap-seq-btn[data-color=cyan]   { color: #00d4ff; }
.cpcap-seq-btn[data-color=pink]   { color: #ff40c0; }
.cpcap-seq-btn[data-color=amber]  { color: #ffaa20; }
.cpcap-seq-btn[data-color=green]  { color: #00ff88; }
.cpcap-seq-btn:hover {
  border-color: currentColor;
  box-shadow: 0 0 14px rgba(255, 255, 255, 0.04), inset 0 0 14px rgba(0, 212, 255, 0.05);
}
.cpcap-seq-btn.flash {
  box-shadow:
    0 0 32px currentColor,
    inset 0 0 24px rgba(255, 255, 255, 0.15);
  border-color: currentColor;
  transform: scale(1.04);
  background:
    radial-gradient(circle at center, rgba(255, 255, 255, 0.12), transparent 70%),
    var(--cpcap-bg);
}
.cpcap-seq-icon {
  display: inline-flex;
  filter: drop-shadow(0 0 6px currentColor);
}
.cpcap-seq-icon svg { display: block; }
.cpcap-seq-tag {
  font-family: 'JetBrains Mono', monospace;
  font-size: 9px;
  letter-spacing: 0.18em;
  color: currentColor;
  opacity: 0.75;
}
.cpcap-seq-progress {
  text-align: center;
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  color: var(--cpcap-text-muted);
  margin-top: 8px;
  letter-spacing: 0.15em;
}

/* ── HoneypotTime ── */
.cpcap-hp-debug {
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  line-height: 1.7;
  background:
    linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.1)),
    var(--cpcap-bg);
  border: 1px solid rgba(0, 212, 255, 0.18);
  border-radius: 4px;
  padding: 12px 14px;
  position: relative;
  overflow: hidden;
}
.cpcap-hp-debug::before {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    0deg,
    rgba(0, 212, 255, 0.04) 0px,
    rgba(0, 212, 255, 0.04) 1px,
    transparent 1px,
    transparent 3px
  );
  pointer-events: none;
}
.cpcap-hp-debug > div { position: relative; z-index: 2; padding: 2px 0; }
.cpcap-hp-honey {
  position: absolute;
  left: -9999px;
  top: -9999px;
  opacity: 0;
  height: 0;
  width: 0;
}

/* ── CodeBug1C ── */
.cpcap-bug-snippets {
  display: grid;
  grid-template-columns: 1fr;
  gap: 10px;
}
.cpcap-bug-snippet {
  background:
    linear-gradient(180deg, rgba(0, 0, 0, 0.25), transparent),
    var(--cpcap-bg);
  border: 1px solid var(--cpcap-border);
  border-radius: 4px;
  padding: 12px 14px 12px 44px;
  cursor: pointer;
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  line-height: 1.55;
  white-space: pre-wrap;
  color: var(--cpcap-text);
  transition: border-color 0.15s, background 0.15s, transform 0.1s;
  position: relative;
  counter-increment: cpcap-snippet;
}
.cpcap-bug-snippet::before {
  content: counter(cpcap-snippet, decimal-leading-zero);
  position: absolute;
  left: 10px;
  top: 12px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  color: var(--cpcap-cyan);
  letter-spacing: 0.1em;
  opacity: 0.6;
}
.cpcap-bug-snippet::after {
  content: '';
  position: absolute;
  left: 28px; top: 12px; bottom: 12px;
  width: 1px;
  background: rgba(0, 212, 255, 0.15);
}
.cpcap-bug-snippets { counter-reset: cpcap-snippet; }
.cpcap-bug-snippet:hover {
  border-color: var(--cpcap-cyan);
  background:
    linear-gradient(180deg, rgba(0, 212, 255, 0.06), rgba(0, 212, 255, 0.02)),
    var(--cpcap-bg);
  transform: translateX(2px);
}
.cpcap-bug-snippet.cpcap-locked-correct {
  border-color: var(--cpcap-signal-green);
  background: rgba(0, 255, 136, 0.08);
  box-shadow: 0 0 16px rgba(0, 255, 136, 0.2);
}
.cpcap-bug-snippet.cpcap-locked-correct::before { color: var(--cpcap-signal-green); opacity: 1; }
.cpcap-bug-snippet.cpcap-locked-wrong {
  border-color: var(--cpcap-hot);
  background: rgba(255, 64, 96, 0.08);
  animation: cpcapShake 0.4s cubic-bezier(0.25, 1, 0.5, 1);
}
.cpcap-bug-snippet.cpcap-locked-wrong::before { color: var(--cpcap-hot); opacity: 1; }
@keyframes cpcapShake {
  0%, 100% { transform: translateX(0); }
  25%      { transform: translateX(-3px); }
  75%      { transform: translateX(3px); }
}

/* ── ScrubReveal ── */
.cpcap-scrub-display {
  font-family: 'JetBrains Mono', monospace;
  font-size: 26px;
  letter-spacing: 0.18em;
  text-align: center;
  padding: 18px 12px;
  min-height: 36px;
  background:
    radial-gradient(ellipse at center, rgba(0, 212, 255, 0.06), transparent 70%),
    linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.1)),
    var(--cpcap-bg);
  border: 1px solid rgba(0, 212, 255, 0.2);
  border-radius: 4px;
  margin-bottom: 14px;
  text-shadow: 0 0 10px rgba(0, 212, 255, 0.4);
  color: var(--cpcap-text);
  position: relative;
  overflow: hidden;
}
.cpcap-scrub-display::before {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    0deg,
    rgba(0, 212, 255, 0.05) 0px,
    rgba(0, 212, 255, 0.05) 1px,
    transparent 1px,
    transparent 3px
  );
  pointer-events: none;
}
.cpcap-scrub-slider {
  width: 100%;
  accent-color: var(--cpcap-cyan);
  height: 4px;
}
.cpcap-scrub-readout {
  display: flex;
  justify-content: space-between;
  font-family: 'JetBrains Mono', monospace;
  font-size: 10px;
  color: var(--cpcap-text-muted);
  margin-top: 8px;
  letter-spacing: 0.1em;
}
.cpcap-scrub-pct {
  color: var(--cpcap-cyan);
  font-weight: 700;
  text-shadow: 0 0 6px rgba(0, 212, 255, 0.4);
}
.cpcap-scrub-target { color: var(--cpcap-amber); }

/* Status badges */
.cpcap-status.cpcap-ok::before    { content: '✓ '; }
.cpcap-status.cpcap-warn::before  { content: '◇ '; }
.cpcap-status.cpcap-error::before { content: '✗ '; }

/* Reduced motion: disable transitions/animations */
@media (prefers-reduced-motion: reduce) {
  .cpcap, .cpcap * {
    transition: none !important;
    animation: none !important;
  }
  .cpcap-decoder-display::before { display: none; }
}

/* ─── demo block wrapper ─── */
.cap-demo-block{margin:18px 0 24px}
.cap-demo-actions{display:flex;gap:8px;margin-top:8px}
.cap-demo-actions button{background:transparent;border:1px solid #00d4ff;color:#00d4ff;font-family:'JetBrains Mono',monospace;font-size:11px;padding:6px 14px;border-radius:4px;cursor:pointer;letter-spacing:0.12em;text-transform:uppercase}
.cap-demo-actions button:hover{background:rgba(0,212,255,0.1)}
.cap-demo-verdict{margin-top:8px;padding:8px 12px;border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:12px;letter-spacing:0.05em;display:none}
.cap-demo-verdict.show{display:block}
.cap-demo-verdict.pass{background:rgba(0,255,136,0.08);border:1px solid #00ff88;color:#00ff88}
.cap-demo-verdict.fail{background:rgba(255,64,96,0.08);border:1px solid #ff4060;color:#ff4060}
</style>
<p>Перестал играть в игры, заметно реже смотрю фильмы. И в целом — почти не отвлекаюсь от разработки. Потому что мои отвлечения — это такая же разработка, как и то, что я делаю на работе. Сам процесс кардинально поменялся. Вместо рутинного залипания в коде — постановка задачи, корректировка, уточнение деталей, визуальная проверка. Убрав всю неинтересную шелуху, я сосредоточился на главном. Это похоже на мышь в эксперименте про кнопку счастья — которая давит и давит, потому что хорошо. Я нашёл эту кнопку у себя. В <a href="/иигла-как-формируется-зависимость-от/">прошлой статье про ИИглу</a> я достаточно точно описал, как формируется эта зависимость. Нормальная ли это ситуация — покажет время. А пока — вот что я придумал в перерывах между тремя другими проектами.</p>
<p>Шесть своих киберпанк-капч. Не потому что у гугла плохая защита — она нормально работает. А потому что хотел: посетитель добрался до формы комментария, написал текст, и в момент проверки «ты точно не робот?» продолжал чувствовать что он на киберпанк-блоге, а не на госуслугах.</p>
<p>Все шесть <strong>прямо в этой статье</strong> ниже — рабочие, можно потыкать. Кнопка «Проверить» покажет вердикт, «Заново» — перезапустит.</p>
<h2>Декодер</h2>
<p>Символы хаотично перемешаны, потом по одному встают на свои места. Через секунду-полторы появляется нормальное слово — введи его в поле. Для простого бота это уже не сценарий «забрал скрытое поле и отправил форму»: нужно выполнять JS, ждать анимацию или разбирать состояние страницы. Не настоящая стена, но от массового мусора отсекает.</p>
<div class="cap-demo-block">
<div id="demo-decoder"></div>
<div class="cap-demo-actions">
    <button data-demo="decoder" data-action="validate">Проверить</button><br />
    <button data-demo="decoder" data-action="replay">Заново</button>
  </div>
<div class="cap-demo-verdict" data-for="decoder"></div>
</div>
<h2>Лава-шар с температурой</h2>
<p>Один ползунок, к которому прицеплен мой же огненный шар (отдельная библиотека, я её делал давно). Тащишь — шар меняет цвет от ледяного синего к раскалённому красному. Над ним пишет «попади в <code>тёплый</code>» — нужно поставить ползунок в правильный диапазон.</p>
<div class="cap-demo-block">
<div id="demo-lava"></div>
<div class="cap-demo-actions">
    <button data-demo="lava" data-action="validate">Проверить</button><br />
    <button data-demo="lava" data-action="replay">Заново</button>
  </div>
<div class="cap-demo-verdict" data-for="lava"></div>
</div>
<h2>Повтори последовательность</h2>
<p>Четыре светящиеся кнопки, пара из них вспыхивает по очереди. Запомнил порядок — повтори, кликая в том же порядке. Как в детской игре «Симон» из 80-х. Если включён режим «убрать анимации» (для чувствительных к мерцанию) — вместо вспышек просто пишется список «1.▴ 2.<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/25b6.png" alt="▶" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 3.<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/25c0.png" alt="◀" class="wp-smiley" style="height: 1em; max-height: 1em;" />».</p>
<div class="cap-demo-block">
<div id="demo-seq"></div>
<div class="cap-demo-actions">
    <button data-demo="seq" data-action="validate">Проверить</button><br />
    <button data-demo="seq" data-action="replay">Заново</button>
  </div>
<div class="cap-demo-verdict" data-for="seq"></div>
</div>
<h2>Невидимая ловушка</h2>
<p>Скрытое от человека поле, которое простые боты часто заполняют (они автоматически пихают значения везде куда могут), плюс таймер: если форма улетела через секунду после открытия страницы — почти наверняка это не человек. Никаких проверок для обычного посетителя, всё происходит само. Ловит массовых ботов, которые тупо обходят формы скриптом — на LLM-ботов это не работает, для них нужны капчи посложнее.</p>
<p>В демо ниже виден отладочный экранчик: «поле пустое: ✓», «прошло секунд: N», «вердикт: ЧЕЛОВЕК / РОБОТ». В реальном использовании это всё скрыто. Кнопка «прикинуться роботом» симулирует поведение бота.</p>
<div class="cap-demo-block">
<div id="demo-hp"></div>
<div class="cap-demo-actions">
    <button data-demo="hp" data-action="validate">Проверить</button><br />
    <button data-demo="hp" data-action="replay">Заново</button>
  </div>
<div class="cap-demo-verdict" data-for="hp"></div>
</div>
<h2>Найди ошибку в 1С-коде</h2>
<p>Три фрагмента кода 1С, в одном — синтаксическая ошибка (пропущен <code>КонецПроцедуры</code>, лишняя скобка, незакрытая кавычка). Кликни на сломанный. Подходит для блога про 1С — массовый формовый спам обычно не разбирает синтаксис конкретного языка, а программист замечает за пять секунд.</p>
<div class="cap-demo-block">
<div id="demo-bug"></div>
<div class="cap-demo-actions">
    <button data-demo="bug" data-action="validate">Проверить</button><br />
    <button data-demo="bug" data-action="replay">Заново</button>
  </div>
<div class="cap-demo-verdict" data-for="bug"></div>
</div>
<h2>Точная позиция</h2>
<p>Ползунок 0–100%. Слово видно только в узкой полоске (например, между 67 и 73%) — выходишь за неё, и оно снова рассыпается на шум. Поймай нужную точку, прочти слово, введи. Главное — нельзя «дотащить до конца и победить», нужна точность.</p>
<div class="cap-demo-block">
<div id="demo-scrub"></div>
<div class="cap-demo-actions">
    <button data-demo="scrub" data-action="validate">Проверить</button><br />
    <button data-demo="scrub" data-action="replay">Заново</button>
  </div>
<div class="cap-demo-verdict" data-for="scrub"></div>
</div>
<h2>Лава-шар — взял свой же npm-пакет</h2>
<p>У меня есть npm-пакет <code>@andromanpro/lava-orb</code> — огненный шарик, который цепляется к ползунку и меняет цвет в зависимости от позиции. Делал давно для другого поста, опубликовал, потом просто лежал.</p>
<p>Капча написалась за час. Цепляю шар к ползунку, проверка на «попал в нужный диапазон» при отправке. Готово.</p>
<p>Что хочется отметить — стандартный ползунок в браузере <strong>уродлив</strong>. Серая полоска и круглый кружок. Стилизация в CSS — отдельный мини-ад: для каждого браузера свои псевдо-селекторы (для хрома один, для огнелиса другой), и каждый со своими тонкостями. Я сделал градиент полоски от ледяного синего к огненному красному (через золотисто-жёлтый посередине), а сам кружок сделал прозрачным, потому что сверху на него всё равно ложится мой огненный шар. Под полоской — отметки температурных зон с подписями.</p>
<p>Если шар по какой-то причине не загрузился — градиентная полоска остаётся и ползунок продолжает работать. Защита не отваливается.</p>
<h2>Точная позиция — почему «до конца дотащил» это скучно</h2>
<p>Сначала написал тривиально: тащи ползунок до 100%, тогда слово полностью видно. На отправке проверка «значение &gt;= 100». Готово.</p>
<p>Запустил, попробовал. Скучно. Каждый раз одно и то же — таскаешь до конца, читаешь, вводишь. Никакого вызова — «дотащил — прошёл».</p>
<p>Подумал. Случайная цель? Каждый раз новое значение, например 60–89, проходишь если ползунок выше или равно. Лучше, но всё равно посетитель может перетащить до 100% и пройти. Никакой точности не нужно.</p>
<p>Финальная версия — <strong>узкая полоска</strong>: цель ±3%. Если ползунок в окне (например 67–73 при цели 70) — слово полностью видно. Перетащил выше или ниже — слово плавно растворяется обратно в шум. На отправке проверяю что ползунок в окне И слово введено правильно.</p>
<p>Получилось то что хотелось — <strong>точность</strong>. Не до конца, не до начала, а в <strong>узкую полоску</strong>. Робот может попробовать перебрать значения и наблюдать за тем что показывается, но это уже сильно дороже простого «тащи до конца».</p>
<h2>Что дальше</h2>
<p>Пакет уже лежит в npm под именем <code>@andromanpro/cyberpunk-captcha</code>, ротация подключена к форме комментариев блога — каждая загрузка поста выдаёт случайный вариант. Если у тебя есть идея для <strong>седьмой</strong> капчи в этом стиле — пиши в комментариях.</p>
<p>Сами механики капч заняли где-то час активного промптинга — без отделки UI, упаковки в npm и интеграции в форму комментариев. То есть «работающий прототип за час», а не «готовый продукт».</p>
<p>Гораздо больше времени занимает редактирование текстов статей — я терпеть не могу как ИИ собирает фразы, а объяснить модели что именно мне не нравится — не могу. Поэтому вот сижу и редактирую. Но мне это тоже нравится. Надеюсь не выгорю)</p>
<hr>
<p>GitHub: <a href="https://github.com/andromanpro/cyberpunk-captcha">github.com/andromanpro/cyberpunk-captcha</a></p>
<p>Живые примеры (отдельный сайт): <a href="https://andromanpro.github.io/cyberpunk-captcha">andromanpro.github.io/cyberpunk-captcha</a></p>
<p>Установка: <code>npm install @andromanpro/cyberpunk-captcha</code></p>
<script src="https://unpkg.com/@andromanpro/lava-orb@0.1.2/dist/lava-orb.js"></script>
<script>
var CyberpunkCaptcha=(()=>{var b=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var O=Object.prototype.hasOwnProperty;var B=(i,t,e)=>t in i?b(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var R=(i,t)=>{for(var e in t)b(i,e,{get:t[e],enumerable:!0})},H=(i,t,e,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of A(t))!O.call(i,s)&&s!==e&&b(i,s,{get:()=>t[s],enumerable:!(a=I(t,s))||a.enumerable});return i};var D=i=>H(b({},"__esModule",{value:!0}),i);var n=(i,t,e)=>B(i,typeof t!="symbol"?t+"":t,e);var j={};R(j,{BaseCaptcha:()=>r,CaptchaRotation:()=>x,CodeBug1C:()=>f,DecoderLock:()=>l,HoneypotTime:()=>m,LavaOrbTemp:()=>d,ScrubReveal:()=>g,SequenceDecode:()=>u});function h(i){return i[Math.floor(Math.random()*i.length)]}function k(i,t){return Math.floor(Math.random()*(t-i+1))+i}function _(i){return String(i).replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t])}function C(){return typeof window!="undefined"&&window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches}function S(){let i=new Map;return{on(t,e){return i.has(t)||i.set(t,new Set),i.get(t).add(e),()=>{var a;return(a=i.get(t))==null?void 0:a.delete(e)}},off(t,e){var a;(a=i.get(t))==null||a.delete(e)},emit(t,...e){let a=i.get(t);if(a)for(let s of a)try{s(...e)}catch(o){console.error(`[cyberpunk-captcha] handler for "${t}" threw:`,o)}},clear(){i.clear()}}}var r=class{constructor(t,e={}){if(!t||!(t instanceof HTMLElement))throw new Error("[cyberpunk-captcha] mount element required");this.mount=t,this.options=e,this.state={},this._emitter=S(),this._destroyed=!1,this._reduced=C(),this.mount.classList.add("cpcap",`cpcap-${this.constructor.id||"base"}`),this.render()}render(){this.mount.innerHTML='<div class="cpcap-error">BaseCaptcha.render() not implemented</div>'}checkValid(){return{pass:!1,message:"BaseCaptcha.checkValid() not implemented"}}isValid(){if(this._destroyed)return!1;let t=this.checkValid();return this._emitter.emit(t.pass?"pass":"fail",t),typeof this.options.onValidate=="function"&&this.options.onValidate(t.pass,t),t.pass}validate(){return this._destroyed?{pass:!1,message:"destroyed"}:this.checkValid()}replay(){this._destroyed||(this.state={},this.mount.innerHTML="",this.render(),this._emitter.emit("replay"))}destroy(){this._destroyed||(this._destroyed=!0,this.mount.innerHTML="",this.mount.classList.remove("cpcap",`cpcap-${this.constructor.id||"base"}`),this._emitter.clear())}on(t,e){return this._emitter.on(t,e)}off(t,e){this._emitter.off(t,e)}emit(t,...e){if(this._emitter.emit(t,...e),t==="change"&&typeof this.options.onChange=="function")try{this.options.onChange(...e)}catch(a){}}};n(r,"name","BaseCaptcha");var N=["ANDROMAN","CYBERPUNK","SIGNAL","MATRIX","NEURAL","AVATAR","HACKER","UPLINK","DECODER","KINDR"],V="#@$%&*+=!?><~:;.-_ABCDEF\u0410\u0411\u0412\u0413\u0414\u0415\u0416\u0418\u041A\u041B\u041C\u041D\u041E\u041F\u0430\u0431\u0432\u0433\u0434\u0435\u0436\u0438\u043A\u043B\u043C\u043D\u043E\u043F0123456789",l=class extends r{render(){let t=this.options.pool||N,e=this.options.noise||V,a=this.options.stepMs||60;if(this.state.expected=h(t),this.state.locked=!1,this.state.noise=e,this.state.stepMs=a,this.mount.innerHTML=`
      <div class="cpcap-label">
        <span>// SIGNAL DECODER</span>
        <span class="cpcap-decoder-status">decoding\u2026</span>
      </div>
      <div class="cpcap-decoder-display"></div>
      <input type="text" class="cpcap-input cpcap-decoder-input" autocomplete="off" placeholder="type the unlocked signal" />
    `,this._display=this.mount.querySelector(".cpcap-decoder-display"),this._statusEl=this.mount.querySelector(".cpcap-decoder-status"),this._input=this.mount.querySelector(".cpcap-decoder-input"),this._input.addEventListener("input",()=>{this._userTyped=!0,this.emit("change")}),this._reduced){this._display.textContent=this.state.expected,this._statusEl.textContent="\u2713 locked",this._statusEl.style.color="var(--cpcap-cyan)",this.state.locked=!0;return}this._userTyped=!1,this._startLoop()}_startLoop(){this._destroyed||this._userTyped||(this._statusEl.textContent="decoding\u2026",this._statusEl.style.color="",this._runDecode(()=>{this._timer=setTimeout(()=>{this._destroyed||this._userTyped||this._runScramble(()=>{this._timer=setTimeout(()=>this._startLoop(),400)})},2500)}))}_runDecode(t){let e=this.state.expected,a=this.state.noise,s=0,o=()=>{if(this._destroyed)return;let p="";for(let c=0;c<e.length;c++)p+=c<s?e.charAt(c):a.charAt(Math.floor(Math.random()*a.length));this._display.textContent=p,s<e.length?(s++,this._timer=setTimeout(o,this.state.stepMs)):(this._statusEl.textContent="\u2713 locked",this._statusEl.style.color="var(--cpcap-cyan)",this.state.locked=!0,this.emit("lock"),t&&t())};o()}_runScramble(t){let e=this.state.expected,a=this.state.noise;this.state.locked=!1;let s=e.length,o=()=>{if(this._destroyed)return;let p="";for(let c=0;c<e.length;c++)p+=c<s?e.charAt(c):a.charAt(Math.floor(Math.random()*a.length));this._display.textContent=p,s>0?(s--,this._timer=setTimeout(o,40)):t&&t()};o()}destroy(){this._timer&&clearTimeout(this._timer),super.destroy()}checkValid(){if(!this.state.locked)return{pass:!1,message:"wait for signal lock"};let t=(this._input.value||"").trim().toUpperCase();return t?t===this.state.expected?{pass:!0,message:`signal accepted: ${this.state.expected}`}:{pass:!1,message:`wrong signal: \xAB${_(t)}\xBB \u2260 \xAB${this.state.expected}\xBB`}:{pass:!1,message:"enter the unlocked signal"}}};n(l,"id","decoder-lock"),n(l,"name","Decoder Lock");var y=[{name:"ICE",min:0,max:15},{name:"COLD",min:16,max:35},{name:"WARM",min:36,max:55},{name:"HOT",min:56,max:75},{name:"FIRE",min:76,max:100}];function $(i){for(let t of y)if(i>=t.min&&i<=t.max)return t;return y[y.length-1]}var d=class extends r{render(){this.state.targetBand=y[Math.floor(Math.random()*y.length)],this.mount.innerHTML=`
      <div class="cpcap-label">
        <span>// SET TEMPERATURE TO:</span>
        <span class="cpcap-lava-target" style="color: var(--cpcap-cyan); font-weight: 700;"></span>
      </div>
      <div class="cpcap-lava-wrap">
        <input type="range" class="cpcap-lava-slider" min="0" max="100" value="50" step="1" />
        <div class="cpcap-lava-bands">
          <span>ICE</span><span>COLD</span><span>WARM</span><span>HOT</span><span>FIRE</span>
        </div>
      </div>
      <div class="cpcap-lava-readout">
        <span class="side">0%</span>
        <span class="current">50% \xB7 WARM</span>
        <span class="side">100%</span>
      </div>
      <div class="cpcap-status"></div>
    `,this._slider=this.mount.querySelector(".cpcap-lava-slider"),this._target=this.mount.querySelector(".cpcap-lava-target"),this._current=this.mount.querySelector(".cpcap-lava-readout .current"),this._status=this.mount.querySelector(".cpcap-status"),this._target.textContent=`${this.state.targetBand.name} (${this.state.targetBand.min}%\u2013${this.state.targetBand.max}%)`,this._slider.addEventListener("input",()=>this._onChange(this._slider.value)),this._onChange(50);let t=this.options.lavaOrb||typeof window!="undefined"&&window.LavaOrb;if(t&&typeof t.attach=="function")try{this._orbHandle=t.attach(this._slider,{size:this.options.orbSize||70,detach:!1,onChange:e=>this._onChange(e)})}catch(e){console.warn("[cyberpunk-captcha] lava-orb attach failed, falling back to gradient slider:",e)}}_onChange(t){t=parseInt(t,10);let e=$(t);this._current.textContent=`${t}% \xB7 ${e.name}`,e.name===this.state.targetBand.name?(this._status.textContent="\u2713 in target band",this._status.className="cpcap-status cpcap-ok"):(this._status.textContent="adjust slider\u2026",this._status.className="cpcap-status"),this.emit("change",t)}destroy(){if(this._orbHandle&&typeof this._orbHandle.destroy=="function")try{this._orbHandle.destroy()}catch(t){}super.destroy()}checkValid(){let t=parseInt(this._slider.value,10),e=$(t);return e.name!==this.state.targetBand.name?{pass:!1,message:`temperature ${t}% (${e.name}) \u2260 target ${this.state.targetBand.name}`}:{pass:!0,message:`temperature: ${t}% \xB7 ${e.name}`}}};n(d,"id","lava-orb-temp"),n(d,"name","Lava-Orb Temperature");var M=["cyan","pink","amber","green"],U={cyan:'<svg viewBox="0 0 36 36" width="34" height="34" aria-hidden="true"><polygon points="18,4 31,11.5 31,24.5 18,32 5,24.5 5,11.5" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linejoin="miter"/><circle cx="18" cy="18" r="3" fill="currentColor"/></svg>',pink:'<svg viewBox="0 0 36 36" width="34" height="34" aria-hidden="true"><circle cx="18" cy="18" r="13" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="18" cy="18" r="7" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="18" cy="18" r="3" fill="currentColor"/></svg>',amber:'<svg viewBox="0 0 36 36" width="34" height="34" aria-hidden="true"><polyline points="5,21 11,12 14,18 18,9 22,18 25,12 31,21" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linejoin="miter" stroke-linecap="square"/></svg>',green:'<svg viewBox="0 0 36 36" width="34" height="34" aria-hidden="true"><rect x="6" y="6" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2"/><line x1="6" y1="14" x2="30" y2="14" stroke="currentColor" stroke-width="1.5"/><line x1="6" y1="22" x2="30" y2="22" stroke="currentColor" stroke-width="1.5"/><line x1="14" y1="6" x2="14" y2="30" stroke="currentColor" stroke-width="1.5"/><line x1="22" y1="6" x2="22" y2="30" stroke="currentColor" stroke-width="1.5"/></svg>'},q={cyan:"01_HEX",pink:"02_CORE",amber:"03_WAVE",green:"04_GRID"},L={cyan:"\u25A2HEX",pink:"\u25C9CORE",amber:"\u3030WAVE",green:"\u25A6GRID"},u=class extends r{render(){let t=this.options.minLen||3,e=this.options.maxLen||5,a=t+Math.floor(Math.random()*(e-t+1));this.state.sequence=[];for(let s=0;s<a;s++)this.state.sequence.push(h(M));this.state.userInput=[],this.state.locked=!1,this.state.solved=!1,this.mount.innerHTML=`
      <div class="cpcap-label">
        <span>// REPEAT THE SEQUENCE</span>
        <span class="cpcap-seq-status">memorize\u2026</span>
      </div>
      <div class="cpcap-seq-pad">
        ${M.map(s=>`<button type="button" class="cpcap-seq-btn" data-color="${s}" aria-label="${q[s]}"><span class="cpcap-seq-icon">${U[s]}</span><span class="cpcap-seq-tag">${q[s]}</span></button>`).join("")}
      </div>
      <div class="cpcap-seq-progress"></div>
      <div style="text-align: center; margin-top: 8px;">
        <button type="button" class="cpcap-button cpcap-seq-show">\u25B7 show again</button>
      </div>
    `,this._pad=this.mount.querySelector(".cpcap-seq-pad"),this._statusEl=this.mount.querySelector(".cpcap-seq-status"),this._progress=this.mount.querySelector(".cpcap-seq-progress"),this._showBtn=this.mount.querySelector(".cpcap-seq-show"),this._pad.addEventListener("click",s=>this._onPadClick(s)),this._showBtn.addEventListener("click",()=>this._showSequence()),this._showSequence()}_flash(t,e=350){let a=this._pad.querySelector(`.cpcap-seq-btn[data-color="${t}"]`);a&&(a.classList.add("flash"),setTimeout(()=>a.classList.remove("flash"),e))}_showSequence(){if(!this._destroyed){if(this.state.locked=!0,this.state.userInput=[],this._statusEl.textContent="watch the sequence\u2026",this._statusEl.style.color="var(--cpcap-text-muted)",this._progress.textContent="",this._reduced){this._progress.innerHTML="click in order: "+this.state.sequence.map((t,e)=>`${e+1}.${L[t]}`).join(" "),this.state.locked=!1,this._statusEl.textContent="repeat sequence";return}this.state.sequence.forEach((t,e)=>{setTimeout(()=>this._flash(t,400),600+e*600)}),setTimeout(()=>{this._destroyed||(this.state.locked=!1,this._statusEl.textContent="repeat sequence",this._progress.textContent=`0 / ${this.state.sequence.length}`)},600+this.state.sequence.length*600+200)}}_onPadClick(t){if(this.state.locked||this.state.solved)return;let e=t.target.closest(".cpcap-seq-btn");if(!e)return;let a=e.getAttribute("data-color");this._flash(a,200),this.state.userInput.push(a);let s=this.state.userInput.length-1;if(this.state.userInput[s]!==this.state.sequence[s]){this._statusEl.textContent="wrong, restart",this._statusEl.style.color="var(--cpcap-hot)",setTimeout(()=>this.replay(),800);return}this._progress.textContent=`${this.state.userInput.length} / ${this.state.sequence.length}`,this.emit("change",this.state.userInput.length),this.state.userInput.length===this.state.sequence.length&&(this.state.solved=!0,this._statusEl.textContent="\u2713 sequence matched",this._statusEl.style.color="var(--cpcap-cyan)")}checkValid(){return this.state.solved?{pass:!0,message:`pattern: ${this.state.sequence.map(t=>L[t]).join(" ")}`}:{pass:!1,message:"sequence not completed"}}};n(u,"id","sequence-decode"),n(u,"name","Sequence Decode");var m=class extends r{render(){this.state.minElapsed=this.options.minElapsed||3,this.state.fieldName=this.options.fieldName||"website2",this.state.startTime=Math.floor(Date.now()/1e3),this.state.showDebug=this.options.showDebug!==!1;let t=`
      <input type="text" class="cpcap-hp-honey cpcap-hp-honey-input" name="${_(this.state.fieldName)}" tabindex="-1" autocomplete="off" />
      <input type="hidden" class="cpcap-hp-ts" value="${this.state.startTime}" />
    `;this.state.showDebug&&(t+=`
        <div class="cpcap-label">
          <span>// HONEYPOT STATUS \xB7 debug overlay</span>
        </div>
        <div class="cpcap-hp-debug"></div>
        <div style="margin-top: 10px;">
          <button type="button" class="cpcap-button cpcap-hp-pretend" style="border-color: var(--cpcap-hot); color: var(--cpcap-hot);">\u26A0 pretend bot</button>
        </div>
      `),this.mount.innerHTML=t,this._honey=this.mount.querySelector(".cpcap-hp-honey-input"),this._ts=this.mount.querySelector(".cpcap-hp-ts"),this._debug=this.mount.querySelector(".cpcap-hp-debug"),this._honey.addEventListener("input",()=>this.emit("change")),this.state.showDebug&&(this._pretend=this.mount.querySelector(".cpcap-hp-pretend"),this._pretend.addEventListener("click",()=>{this._honey.value="http://spam-site.example.com",this._ts.value=Math.floor(Date.now()/1e3),this._updateDebug()}),this._timer=setInterval(()=>this._updateDebug(),250),this._updateDebug())}_updateDebug(){if(!this._debug)return;let e=Math.floor(Date.now()/1e3)-parseInt(this._ts.value,10),a=this._honey.value==="",s=a&&e>=this.state.minElapsed?"HUMAN":"BOT",o=s==="HUMAN"?"var(--cpcap-signal-green)":"var(--cpcap-hot)";this._debug.innerHTML=`<div>field empty: <span style="color: ${a?"var(--cpcap-signal-green)":"var(--cpcap-hot)"}">${a?"\u2713 true":"\u2717 false (\xAB"+_(this._honey.value.substring(0,30))+"\xBB)"}</span></div><div>elapsed: <span style="color: ${e>=this.state.minElapsed?"var(--cpcap-signal-green)":"var(--cpcap-hot)"}">${e}s${e>=this.state.minElapsed?" \u2713":` (need \u2265${this.state.minElapsed})`}</span></div><div>verdict: <span style="color: ${o}; font-weight: bold">${s}</span></div>`}destroy(){this._timer&&clearInterval(this._timer),super.destroy()}checkValid(){let t=Math.floor(Date.now()/1e3)-parseInt(this._ts.value,10);return this._honey.value!==""?{pass:!1,message:"honeypot triggered (bot detected)"}:t<this.state.minElapsed?{pass:!1,message:`too fast (${t}s < ${this.state.minElapsed}s)`}:{pass:!0,message:`verified (${t}s elapsed)`}}};n(m,"id","honeypot-time"),n(m,"name","Honeypot + Time-based");var P=[[{code:`\u041F\u0440\u043E\u0446\u0435\u0434\u0443\u0440\u0430 \u0422\u0435\u0441\u0442()
  \u0421\u043E\u043E\u0431\u0449\u0438\u0442\u044C("\u041F\u0440\u0438\u0432\u0435\u0442");
\u041A\u043E\u043D\u0435\u0446\u041F\u0440\u043E\u0446\u0435\u0434\u0443\u0440\u044B`,bug:!1},{code:`\u0415\u0441\u043B\u0438 \u0430 > 0 \u0422\u043E\u0433\u0434\u0430
  \u0421\u043E\u043E\u0431\u0449\u0438\u0442\u044C("+");
\u041A\u043E\u043D\u0435\u0446\u0415\u0441\u043B\u0438;`,bug:!1},{code:`\u041F\u0440\u043E\u0446\u0435\u0434\u0443\u0440\u0430 \u0422\u0435\u0441\u0442()
  \u0421\u043E\u043E\u0431\u0449\u0438\u0442\u044C("\u041F\u0440\u0438\u0432\u0435\u0442");`,bug:!0}],[{code:`\u0422\u043E\u0432\u0430\u0440\u044B = \u041D\u043E\u0432\u044B\u0439 \u041C\u0430\u0441\u0441\u0438\u0432;
\u0422\u043E\u0432\u0430\u0440\u044B.\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C("\u041F\u0438\u0446\u0446\u0430");`,bug:!1},{code:`\u0422\u043E\u0432\u0430\u0440\u044B = \u041D\u043E\u0432\u044B\u0439 \u041C\u0430\u0441\u0441\u0438\u0432(;
\u0422\u043E\u0432\u0430\u0440\u044B.\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C("\u041F\u0438\u0446\u0446\u0430");`,bug:!0},{code:`\u0422\u043E\u0432\u0430\u0440\u044B = \u041D\u043E\u0432\u044B\u0439 \u041C\u0430\u0441\u0441\u0438\u0432;
\u0422\u043E\u0432\u0430\u0440\u044B.\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C();`,bug:!1}],[{code:`\u0417\u0430\u043F\u0440\u043E\u0441 = \u041D\u043E\u0432\u044B\u0439 \u0417\u0430\u043F\u0440\u043E\u0441;
\u0417\u0430\u043F\u0440\u043E\u0441.\u0422\u0435\u043A\u0441\u0442 = "\u0412\u042B\u0411\u0420\u0410\u0422\u042C * \u0418\u0417 \u0421\u043F\u0440\u0430\u0432\u043E\u0447\u043D\u0438\u043A.\u041D\u043E\u043C\u0435\u043D\u043A\u043B\u0430\u0442\u0443\u0440\u0430";`,bug:!1},{code:`\u0417\u0430\u043F\u0440\u043E\u0441 = \u041D\u043E\u0432\u044B\u0439 \u0417\u0430\u043F\u0440\u043E\u0441
\u0417\u0430\u043F\u0440\u043E\u0441.\u0422\u0435\u043A\u0441\u0442 = "\u0412\u042B\u0411\u0420\u0410\u0422\u042C";`,bug:!0},{code:`\u0417\u0430\u043F\u0440\u043E\u0441 = \u041D\u043E\u0432\u044B\u0439 \u0417\u0430\u043F\u0440\u043E\u0441;
\u0420\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442 = \u0417\u0430\u043F\u0440\u043E\u0441.\u0412\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C();`,bug:!1}],[{code:`\u0414\u043B\u044F \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0421\u0442\u0440 \u0418\u0437 \u0422\u0430\u0431\u043B\u0438\u0446\u0430 \u0426\u0438\u043A\u043B
  \u0421\u043E\u043E\u0431\u0449\u0438\u0442\u044C(\u0421\u0442\u0440.\u0418\u043C\u044F);
\u041A\u043E\u043D\u0435\u0446\u0426\u0438\u043A\u043B\u0430;`,bug:!1},{code:`\u0414\u043B\u044F \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0421\u0442\u0440 \u0418\u0437 \u0422\u0430\u0431\u043B\u0438\u0446\u0430 \u0426\u0438\u043A\u043B
  \u0421\u043E\u043E\u0431\u0449\u0438\u0442\u044C(\u0421\u0442\u0440.\u0418\u043C\u044F);`,bug:!0},{code:`\u041F\u043E\u043A\u0430 \u0421\u0447\u0451\u0442\u0447\u0438\u043A < 10 \u0426\u0438\u043A\u043B
  \u0421\u0447\u0451\u0442\u0447\u0438\u043A = \u0421\u0447\u0451\u0442\u0447\u0438\u043A + 1;
\u041A\u043E\u043D\u0435\u0446\u0426\u0438\u043A\u043B\u0430;`,bug:!1}],[{code:'\u0421\u043E\u043E\u0431\u0449\u0438\u0442\u044C("\u041F\u0440\u0438\u0432\u0435\u0442, " + \u0418\u043C\u044F\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F);',bug:!1},{code:`\u0421\u043E\u043E\u0431\u0449\u0438\u0442\u044C("\u041F\u0440\u0438\u0432\u0435\u0442, " + \u0418\u043C\u044F\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F);
  \u0421\u043E\u043E\u0431\u0449\u0438\u0442\u044C(\u0414\u0430\u0442\u0430);`,bug:!1},{code:'\u0421\u043E\u043E\u0431\u0449\u0438\u0442\u044C("\u041F\u0440\u0438\u0432\u0435\u0442, " + \u0418\u043C\u044F\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F;',bug:!0}]],f=class extends r{render(){let t=this.options.sets||P;this.state.set=h(t).slice().sort(()=>Math.random()-.5),this.state.solved=!1,this.mount.innerHTML=`
      <div class="cpcap-label">
        <span>// CLICK THE BROKEN TRANSMISSION</span>
        <span class="cpcap-bug-status">scan\u2026</span>
      </div>
      <div class="cpcap-bug-snippets">
        ${this.state.set.map(e=>`<div class="cpcap-bug-snippet" data-bug="${e.bug?"1":"0"}">${this._escape(e.code)}</div>`).join("")}
      </div>
    `,this._snippetsRoot=this.mount.querySelector(".cpcap-bug-snippets"),this._statusEl=this.mount.querySelector(".cpcap-bug-status"),this._snippetsRoot.addEventListener("click",e=>this._onClick(e))}_escape(t){return String(t).replace(/[&<>"']/g,e=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[e])}_onClick(t){if(this.state.solved)return;let e=t.target.closest(".cpcap-bug-snippet");if(!e)return;e.getAttribute("data-bug")==="1"?(e.classList.add("cpcap-locked-correct"),this._statusEl.textContent="\u2713 broken transmission spotted",this._statusEl.style.color="var(--cpcap-signal-green)",this.state.solved=!0,this.emit("change",!0)):(e.classList.add("cpcap-locked-wrong"),this._statusEl.textContent="\u2717 this one valid, scan others",this._statusEl.style.color="var(--cpcap-hot)")}checkValid(){return this.state.solved?{pass:!0,message:"bug spotted \u2014 1\u0421 verified"}:{pass:!1,message:"find the broken snippet"}}};n(f,"id","code-bug-1c"),n(f,"name","1\u0421 Code \u2014 Spot the Bug");var z=["LAVA","ORBS","NEON","CYBER","VECTOR","PIXEL","GHOST","ECHO","SIGNAL","MATRIX"],T="\u2588\u2593\u2592\u2591#@$%&*+=!?>~",g=class extends r{render(){this.state.pool=this.options.pool||z,this.state.windowSize=this.options.windowSize||3;let t=this.options.targetMin||30,e=this.options.targetMax||89;this.state.expected=h(this.state.pool),this.state.target=k(t,e),this.mount.innerHTML=`
      <div class="cpcap-label">
        <span>// SCRUB-DECODE</span>
        <span class="cpcap-scrub-status">find sweet spot\u2026</span>
      </div>
      <div class="cpcap-scrub-display"></div>
      <input type="range" class="cpcap-scrub-slider" min="0" max="100" value="0" step="1" />
      <div class="cpcap-scrub-readout">
        <span>0%</span>
        <span class="cpcap-scrub-pct">0%</span>
        <span>window: <span class="cpcap-scrub-target">${this.state.target-this.state.windowSize}\u2013${this.state.target+this.state.windowSize}%</span></span>
      </div>
      <div style="margin-top: 14px;">
        <input type="text" class="cpcap-input cpcap-scrub-input" autocomplete="off" placeholder="type the revealed signal" />
      </div>
    `,this._slider=this.mount.querySelector(".cpcap-scrub-slider"),this._display=this.mount.querySelector(".cpcap-scrub-display"),this._pct=this.mount.querySelector(".cpcap-scrub-pct"),this._statusEl=this.mount.querySelector(".cpcap-scrub-status"),this._input=this.mount.querySelector(".cpcap-scrub-input"),this._slider.addEventListener("input",()=>this._update()),this._input.addEventListener("input",()=>this.emit("change")),this._update()}_update(){let t=parseInt(this._slider.value,10);this._pct.textContent=`${t}%`;let e=this.state.windowSize,a=this.state.target,s;t>=a-e&&t<=a+e?s=0:t<a-e?s=a-e-t:s=t-(a+e);let o;s===0?o=1:s>=30?o=0:o=1-s/30;let p=this.state.expected,c=Math.round(p.length*o),E="";for(let v=0;v<p.length;v++)v<c?E+=p.charAt(v):E+=T.charAt(Math.floor(Math.random()*T.length));this._display.textContent=E,s===0?(this._statusEl.textContent=`\u2713 in sweet spot \xB7 ${t}%`,this._statusEl.className="cpcap-scrub-status cpcap-ok",this._statusEl.style.color="var(--cpcap-cyan)"):s<5?(this._statusEl.textContent=`almost there \xB7 ${t}% (off by ${s})`,this._statusEl.style.color="var(--cpcap-text-dim)"):o>.4?(this._statusEl.textContent=`getting closer \xB7 ${t}%`,this._statusEl.style.color="var(--cpcap-text-muted)"):(this._statusEl.textContent=`too far \xB7 ${t}%`,this._statusEl.style.color="var(--cpcap-text-muted)"),this.emit("change",t)}checkValid(){let t=parseInt(this._slider.value,10),e=this.state.windowSize,a=this.state.target;if(t<a-e||t>a+e)return{pass:!1,message:`out of sweet spot: ${t}% \u2209 [${a-e}\u2013${a+e}]`};let s=(this._input.value||"").trim().toUpperCase();return s?s===this.state.expected?{pass:!0,message:`sweet spot hit: ${t}% \xB7 signal: ${this.state.expected}`}:{pass:!1,message:`wrong: \xAB${_(s)}\xBB \u2260 \xAB${this.state.expected}\xBB`}:{pass:!1,message:"type the revealed signal"}}};n(g,"id","scrub-reveal"),n(g,"name","Scrub Reveal");var w={"decoder-lock":l,"lava-orb-temp":d,"sequence-decode":u,"honeypot-time":m,"code-bug-1c":f,"scrub-reveal":g},x=class{constructor(t,e={}){this.mount=t,this.opts=e;let a=e.variants||Object.keys(w);if(this.variants=a.filter(c=>w[c]),this.variants.length===0)throw new Error("[cyberpunk-captcha] no valid variants");let s;if(e.pickStrategy==="session"&&typeof sessionStorage!="undefined"){let c=e.sessionKey||"cpcap-pick";s=sessionStorage.getItem(c),(!s||!this.variants.includes(s))&&(s=this.variants[Math.floor(Math.random()*this.variants.length)],sessionStorage.setItem(c,s))}else s=this.variants[Math.floor(Math.random()*this.variants.length)];this.pickedId=s;let o=w[s],p=e.options&&e.options[s]||{};this.captcha=new o(t,p)}isValid(){return this.captcha.isValid()}validate(){return this.captcha.validate()}replay(){this.captcha.replay()}destroy(){this.captcha.destroy()}on(t,e){return this.captcha.on(t,e)}off(t,e){this.captcha.off(t,e)}};typeof window!="undefined"&&(window.CyberpunkCaptcha=window.CyberpunkCaptcha||{BaseCaptcha:r,DecoderLock:l,LavaOrbTemp:d,SequenceDecode:u,HoneypotTime:m,CodeBug1C:f,ScrubReveal:g,CaptchaRotation:x});return D(j);})();
</script>
<script>
(function(){
  function ready(fn){ document.readyState!=='loading' ? fn() : document.addEventListener('DOMContentLoaded', fn); }
  ready(function(){
    var C = window.CyberpunkCaptcha; if(!C) return;
    var demos = {
      decoder: { mount: 'demo-decoder', cls: C.DecoderLock,    opts: {} },
      lava:    { mount: 'demo-lava',    cls: C.LavaOrbTemp,    opts: {} },
      seq:     { mount: 'demo-seq',     cls: C.SequenceDecode, opts: {} },
      hp:      { mount: 'demo-hp',      cls: C.HoneypotTime,   opts: { showDebug: true } },
      bug:     { mount: 'demo-bug',     cls: C.CodeBug1C,      opts: {} },
      scrub:   { mount: 'demo-scrub',   cls: C.ScrubReveal,    opts: {} }
    };
    var instances = {};
    Object.keys(demos).forEach(function(key){
      var d = demos[key]; var el = document.getElementById(d.mount);
      if(!el || !d.cls) return;
      try { instances[key] = new d.cls(el, d.opts); }
      catch(e){ el.innerHTML = '<div style="padding:12px;color:#ff4060;font-family:monospace;font-size:12px;">// init failed: '+e.message+'</div>'; }
    });
    document.addEventListener('click', function(e){
      var btn = e.target.closest('button[data-demo]'); if(!btn) return;
      var key = btn.dataset.demo, action = btn.dataset.action;
      var inst = instances[key]; if(!inst) return;
      var verdict = document.querySelector('.cap-demo-verdict[data-for="'+key+'"]');
      if(action === 'replay'){
        inst.replay(); if(verdict){ verdict.className = 'cap-demo-verdict'; verdict.textContent = ''; }
      } else if(action === 'validate'){
        var r = inst.validate();
        if(verdict){
          verdict.className = 'cap-demo-verdict show ' + (r.pass ? 'pass' : 'fail');
          verdict.textContent = (r.pass ? '✓ ' : '✗ ') + (r.message || (r.pass ? 'pass' : 'fail'));
        }
      }
    });
  });
})();
</script>


<p style="margin-top:1em;font-family:monospace;font-size:0.9em;">→ <a href="https://androman.pro/2026/04/30/cyberpunk-captcha/">открыть на androman.pro</a></p>]]></content:encoded>
					
					<wfw:commentRss>https://androman.pro/2026/04/30/cyberpunk-captcha/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Когда заниматься 1С становится скучно</title>
		<link>https://androman.pro/2026/04/28/1c-stalo-skuchno/</link>
					<comments>https://androman.pro/2026/04/28/1c-stalo-skuchno/#respond</comments>
		
		<dc:creator><![CDATA[Androman.pro]]></dc:creator>
		<pubDate>Sun, 26 Apr 2026 21:00:00 +0000</pubDate>
				<category><![CDATA[дев-лог]]></category>
		<category><![CDATA[лаборатория]]></category>
		<category><![CDATA[1С]]></category>
		<category><![CDATA[Canvas]]></category>
		<category><![CDATA[creative-code]]></category>
		<category><![CDATA[npm]]></category>
		<category><![CDATA[vibe-coding]]></category>
		<category><![CDATA[WebGL]]></category>
		<guid isPermaLink="false">https://androman.pro/2026/04/27/1c-stalo-skuchno/</guid>

					<description><![CDATA[1С-разработчик, не знающий что такое display:flex, выкатил 7 WebGL/Canvas проектов за 5 недель. Потому что заскучал на ползунке опросника.]]></description>
										<content:encoded><![CDATA[<style>
.lava-orb-demo input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  height: 12px;
  background: linear-gradient(to right, #3b82f6 0%, #06b6d4 25%, #fbbf24 60%, #ef4444 100%);
  border-radius: 6px;
  border: 1px solid rgba(0, 212, 255, 0.35);
  box-shadow: 0 0 12px rgba(0, 212, 255, 0.2);
  outline: none;
  cursor: pointer;
}
.lava-orb-demo input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background: transparent;
  opacity: 0;
  cursor: grab;
}
.lava-orb-demo input[type="range"]::-moz-range-thumb {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background: transparent;
  border: 0;
  opacity: 0;
  cursor: grab;
}
</style>
<div class="lava-orb-demo" style="text-align:center;padding:36px 24px;background:rgba(8,20,36,0.6);border:1px solid var(--border);border-radius:12px;margin:32px 0;">
<div style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text-muted);letter-spacing:0.25em;margin-bottom:6px;">// LIVE DEMO · ДВА ОРБА</div>
<div style="font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--text-dim);letter-spacing:0.05em;margin-bottom:24px;">дёрни резко — оторвутся от ползунка → сведи вместе — взорвутся</div>
<div style="max-width:480px;margin:0 auto 48px;padding:0 40px;">
    <input type="range" min="0" max="10" value="3" step="1" id="lo-demo-slider-1" style="width:100%;"></p>
<div style="text-align:center;margin-top:14px;font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:0.2em;"><span id="lo-demo-label-1">холодный</span> · <span id="lo-demo-val-1" style="font-size:18px;">3</span></div>
</p></div>
<div style="max-width:480px;margin:0 auto;padding:0 40px;">
    <input type="range" min="0" max="10" value="7" step="1" id="lo-demo-slider-2" style="width:100%;"></p>
<div style="text-align:center;margin-top:14px;font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:0.2em;"><span id="lo-demo-label-2">тёплый</span> · <span id="lo-demo-val-2" style="font-size:18px;">7</span></div>
</p></div>
</div>
<script src="https://unpkg.com/@andromanpro/lava-orb@latest/dist/lava-orb.js"></script>
<script>
(function(){
  // value (0-10) → { label, color, glow }
  function temp(v){
    var n = +v;
    if (n <= 1)  return { label: 'мороз',       color: '#3b82f6', glow: 'rgba(59,130,246,0.5)' };
    if (n <= 3)  return { label: 'холодный',    color: '#06b6d4', glow: 'rgba(6,182,212,0.5)'  };
    if (n <= 5)  return { label: 'прохладный',  color: '#84cc16', glow: 'rgba(132,204,22,0.5)' };
    if (n <= 7)  return { label: 'тёплый',      color: '#fbbf24', glow: 'rgba(251,191,36,0.5)' };
    if (n <= 9)  return { label: 'горячий',     color: '#f97316', glow: 'rgba(249,115,22,0.5)' };
    return         { label: 'огонь',         color: '#ef4444', glow: 'rgba(239,68,68,0.5)'  };
  }
  function update(v, labelEl, valEl){
    var t = temp(v);
    valEl.textContent = v;
    labelEl.textContent = t.label;
    labelEl.style.color = t.color;
    valEl.style.color = t.color;
    valEl.style.textShadow = '0 0 12px ' + t.glow;
  }
  function init(){
    var i1 = document.getElementById('lo-demo-slider-1');
    var v1 = document.getElementById('lo-demo-val-1');
    var l1 = document.getElementById('lo-demo-label-1');
    var i2 = document.getElementById('lo-demo-slider-2');
    var v2 = document.getElementById('lo-demo-val-2');
    var l2 = document.getElementById('lo-demo-label-2');
    if (!i1 || !i2 || !window.LavaOrb) { setTimeout(init, 200); return; }
    var group = (typeof LavaOrb.createGroup === 'function')
      ? LavaOrb.createGroup({ name: 'demo' })
      : undefined;
    LavaOrb.attach(i1, { size: 60, group: group, detach: true, onChange: function(v){ update(v, l1, v1); } });
    LavaOrb.attach(i2, { size: 60, group: group, detach: true, onChange: function(v){ update(v, l2, v2); } });
    update(i1.value, l1, v1);
    update(i2.value, l2, v2);
  }
  init();
})();
</script>
<hr>
<p>В общем делаю автоматический воркфлоу по анализу бизнес-процессов в 1С для обследования предприятий. Добавил генерацию опросника — можно выбрать роль или должность, протыкать варианты ответов на вопросы, которые были выявлены ранее на основе анализа конфигурации и документации, и выгрузить всё в JSON. Не нужно тратить время на многочасовые интервью.</p>
<p>Но дело не в этом.</p>
<p>Посмотрел я, с какой скоростью с ИИ получится сделать опросник. Добавил тёмную тему на выбор, контроль неотвеченных вопросов, даже салют при завершении. Всё, можно закрывать.</p>
<p>А потом мой взгляд упал на скучные ползунки выбора приоритетов.</p>
<p>Тут-то всё и заверте…</p>
<hr>
<p>Наступили выходные, а ощущение незавершённости не отпускало. Обычный ползунок от 1 до 10 — это же просто цифра, не показательно.</p>
<p>Пришлось развивать — прототип, другой, третий. Покрутил настройки частиц. Узнал что есть Canvas 2D и WebGL 2 (ну как узнал — просто что они существуют). Добавил DOOM-fire (прям как из культовой игры, ну вы поняли) режим как альтернативный движок пламени. Попробовал сделать взрывы, физику воды, отрывающиеся от ползунка орбы, которые улетают по дуге.</p>
<p>Задача была в том числе — проверить, получится ли через ИИ сделать что-то в незнакомой области. Без глубокого погружения в шейдеры и прочие термины которые я впервые увидел на прошлой неделе.</p>
<p>Получилось.</p>
<p>Я думал — сделаю пару эффектов на ползунок и пойду обратно к опроснику.</p>
<p><strong>Что выросло за 5 недель параллельной работы:</strong></p>
<ul>
<li><strong>npm-библиотека <code>@andromanpro/lava-orb</code></strong> — привязывается к любому <code>&lt;input type="range"&gt;</code> одной строкой. От мороза до огня, взрывов и молний.</li>
<li><strong>2 одностраничные игры</strong> — ORB 2048 (жидкие сливающиеся сферы с процедурной музыкой) и ORB ARKANOID (арканоид где блоки — орбы с физикой волн). Оба работают офлайн одним HTML-файлом.</li>
<li><strong>2 интерактивных редактора</strong> — для тонкой настройки жидкости и огня в реальном времени. С экспортом пресетов.</li>
<li><strong>Библиотека из 7 WebGL2-фонов</strong> — drop-in для любой страницы.</li>
</ul>
<p>Всё это — MIT, на GitHub.</p>
<p><img decoding="async" alt="От холода к огню — один и тот же ползунок, разные значения" src="https://raw.githubusercontent.com/andromanpro/lava-orb/main/article/gifs/02-cold-to-hot.gif"></p>
<p>Ни одного профильного курса по веб-разработке я не проходил. Я вообще не знаю, что такое <code>box-sizing</code> и <code>display: flex</code>)))</p>
<hr>
<p>Помните 2048? Ту простую головоломку с плитками, которая в 2014-м разошлась по всем сторам за несколько недель. Сделал свою версию — с жидкими светящимися сферами вместо цифровых плиток. И залип.</p>
<p><strong>Геймплей:</strong><br />
&#8212; Классическая сетка 4×4, свайпы / стрелки / WASD<br />
&#8212; Ячейки — жидкие metaballs через marching squares, сливаются как капли ртути<br />
&#8212; На высоких значениях — процедурный огонь частицами с glow и затуханием<br />
&#8212; При 2048 — <strong>supernova</strong>: space warp, chromatic aberration, 250 частиц, тряска камеры<br />
&#8212; Игра продолжается — 4096, 8192, логотип подхватывает</p>
<p><strong>Звук:</strong><br />
8 процедурных треков через Web Audio API, никаких mp3. Oscillator + filter + reverb синтезируются в браузере. Адаптивный темп — чем больше ячеек, тем быстрее.</p>
<p><strong>Рендеринг:</strong><br />
WebGL2 с Canvas2D fallback. 4 фона на выбор (Cyberpunk / Space / Dark / Synthwave). RU/EN локализация.</p>
<p><strong>Бот / Autoplay</strong> — запускаешь, смотришь как играет сам. Максимум набирает <strong>65 536</strong> (16 слияний — дальше на сетке 4×4 места нет, но есть настройка для большей сетки!).</p>
<p>Для СДВГ-шников — идеальная сенсорная подкормка. Запускаешь на втором мониторе или в углу экрана, свернул — периферическое зрение ловит постоянное мельтешение жидких сфер, мозгу хорошо, на основной задаче сидишь ровнее.</p>
<p>Режим отладки оставил прямо в сборке — FPS, хитбоксы, статистика. Не стал прятать от прода, пусть смотрит кто хочет.</p>
<p>Один файл <code>Orb-2048.html</code> ~330 KB. Скачал на телефон в самолёт — играешь офлайн.</p>
<div class="game-embed" data-src="https://andromanpro.github.io/orb-2048/" data-label="ЗАПУСТИТЬ ORB 2048" style="position:relative;cursor:pointer;border:1px solid var(--border);border-radius:8px;overflow:hidden;background:#030712;margin:24px 0;">
  <img decoding="async" src="https://raw.githubusercontent.com/andromanpro/orb-2048/main/media/gameplay1.gif" alt="ORB 2048 gameplay" style="display:block;width:100%;height:auto;"></p>
<div class="game-embed-overlay" style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(3,7,18,0.55);transition:background 0.2s;">
    <button type="button" style="background:var(--cyan);color:#030712;border:none;padding:14px 28px;font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:600;border-radius:4px;cursor:pointer;letter-spacing:0.15em;"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/25b6.png" alt="▶" class="wp-smiley" style="height: 1em; max-height: 1em;" /> ЗАПУСТИТЬ ORB 2048</button>
  </div>
</div>
<p><strong>Live:</strong> <a href="https://andromanpro.github.io/orb-2048/">andromanpro.github.io/orb-2048</a></p>
<hr>
<p>Если 2048 — про «плавное слияние», то арканоид — про «разрушение». Идея: блоки — это не кубики, а светящиеся орбы-капсулы с жидкостью. Она реагирует на удар волной, а при разрушении выливается вниз, передавая волну соседям.</p>
<p>Так и сделал.</p>
<p><strong>Геймплей:</strong><br />
&#8212; 15 уровней, каждый со своей темой, музыкой и фоном<br />
&#8212; 9 типов орбов — Fire, Ice, Water, Earth, Lava, Steel, Gold, TNT, Rainbow (у каждого своя физика и звук удара)<br />
&#8212; 8 power-up — расширение/сужение платформы, огненный шар, мультишар, прилипание, лазерные турели, жизнь, замедление времени<br />
&#8212; Комбо-система и локальная таблица рекордов</p>
<p><strong>Технологии:</strong><br />
&#8212; WebGL2 SDF-шейдер для орбов — круглый SDF, 2 слоя жидкости с Catmull-Rom интерполяцией, спекулярный блик<br />
&#8212; Verlet 6-колоночная пружинная симуляция волн<br />
&#8212; Процедурная музыка на каждый уровень</p>
<p><strong>Тюнинг шарика:</strong><br />
Тип хвоста (FIRE / LAVA / PLASMA / ICE), длина S/M/L, размер S/M/L. Превью шарика в настройках — живая анимация на весь экран, клик для запуска.</p>
<p><strong>Бот</strong> со скоростью ×2.5. Прицеливается в конкретные орбы, проходит уровни сам. Можно включить и бесконечный режим — уровни продолжают генерироваться сверх 15 штук, бот разбивает бесконечно. Играет лучше меня.</p>
<p>Запустил бота в бесконечном режиме, свернул окно в угол — и пока работаешь в основном, боковым зрением ловишь как шарик дробит орбы, жидкость выливается, комбо растут.</p>
<p>Режим отладки тоже оставил в сборке — FPS, хитбоксы, прыжки между уровнями через <code>[</code> и <code>]</code>. Dev-инструменты не прятал.</p>
<p><strong>97 unit-тестов</strong> встроенных в <code>game.js</code>. Я сам не представлял, что в браузерной игре буду их писать. Но если разрабатывать <strong>с</strong> ИИ — приходится проверять каждый шаг.</p>
<p>Один файл ~225 KB, работает офлайн.</p>
<div class="game-embed" data-src="https://andromanpro.github.io/orb-arkanoid/" data-label="ЗАПУСТИТЬ ORB ARKANOID" style="position:relative;cursor:pointer;border:1px solid var(--border);border-radius:8px;overflow:hidden;background:#030712;margin:24px 0;">
  <img decoding="async" src="https://raw.githubusercontent.com/andromanpro/orb-arkanoid/main/media/gameplay.gif" alt="ORB ARKANOID gameplay" style="display:block;width:100%;height:auto;"></p>
<div class="game-embed-overlay" style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(3,7,18,0.55);transition:background 0.2s;">
    <button type="button" style="background:var(--cyan);color:#030712;border:none;padding:14px 28px;font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:600;border-radius:4px;cursor:pointer;letter-spacing:0.15em;"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/25b6.png" alt="▶" class="wp-smiley" style="height: 1em; max-height: 1em;" /> ЗАПУСТИТЬ ORB ARKANOID</button>
  </div>
</div>
<p><strong>Live:</strong> <a href="https://andromanpro.github.io/orb-arkanoid/">andromanpro.github.io/orb-arkanoid</a></p>
<p><strong>Важный юридический момент.</strong> Arkanoid — зарегистрированная торговая марка Taito с 1986 года (легендарная японская компания, сейчас в составе Square Enix). Моя игра называется ORB ARKANOID чисто как дань уважения жанру — пока это open-source прикол, никто не придёт.</p>
<p>Но если я начну рубить с этого капусту — Taito быстро найдёт меня через юристов. Код можно форкать (MIT разрешает), но имя «Arkanoid» — не моё чтобы его дарить. Для коммерческого форка придётся переименовать в что-то нейтральное вроде <strong>ORB BREAKOUT</strong> (слово «breakout» — общий жанр, не товарный знак), поменять логотип — и тогда можно идти в сторы.</p>
<p>Это к вопросу «а можно я возьму и продам» — в принципе можно, но с переименованием.</p>
<hr>
<p>Решил посчитать — сколько я реально на это потратил.</p>
<p>Прошёлся по логам сессий Claude Code — там у каждого сообщения timestamp. Если между сообщениями меньше 2 минут — это активная работа. Больше 2 минут — отошёл или задумался без ИИ.</p>
<p><strong>Итог: 41 час активного общения. На 7 артефактов, 40 981 строку кода.</strong></p>
<p>Для сравнения, 1С-разработчику без веб-опыта (это я) без ИИ на это ушло бы:<br />
&#8212; 3 месяца на изучение стека (HTML / CSS / JS / Canvas2D / WebGL2 / WebAudio)<br />
&#8212; Ещё ~6 месяцев на реализацию</p>
<p>Грубая оценка — около 8 месяцев full-time вместо 5 недель коротких периодических сессий. Не научная цифра, просто мой внутренний «сколько бы я на это убил, если бы шёл один».</p>
<p>Ускорение не в «ИИ пишет код за тебя». Оно в том, что ИИ убирает <strong>барьер незнания</strong>. Можешь формулировать задачу словами и сразу получать рабочий прототип — вместо того чтобы тонуть в документации к шейдерам, которая предполагает, что ты уже знаешь что такое SDF, metaballs и Verlet integration.</p>
<p>Для меня это самая сильная зона ИИ: незнакомые специализированные домены, где сначала нужно месяцами накапливать словарь, а только потом начинать делать.</p>
<script>
(function(){
  document.querySelectorAll('.game-embed').forEach(function(el){
    var overlay = el.querySelector('.game-embed-overlay');
    if (overlay) {
      el.addEventListener('mouseenter', function(){ overlay.style.background = 'rgba(3,7,18,0.25)'; });
      el.addEventListener('mouseleave', function(){ overlay.style.background = 'rgba(3,7,18,0.55)'; });
    }
    el.addEventListener('click', function(){
      var src = el.dataset.src;
      if (!src) return;
      var iframe = document.createElement('iframe');
      iframe.src = src;
      iframe.setAttribute('allow', 'autoplay; fullscreen');
      iframe.setAttribute('loading', 'lazy');
      iframe.style.cssText = 'display:block;width:100%;height:700px;border:none;';
      el.innerHTML = '';
      el.appendChild(iframe);
      el.style.cursor = 'default';
    });
  });
})();
</script>


<p style="margin-top:1em;font-family:monospace;font-size:0.9em;">→ <a href="https://androman.pro/2026/04/28/1c-stalo-skuchno/">открыть на androman.pro</a></p>]]></content:encoded>
					
					<wfw:commentRss>https://androman.pro/2026/04/28/1c-stalo-skuchno/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
