Some code survived. The rest became documentation.

My earliest foray into coding began with the purchase of a Commodore 64 and an Amiga 500 – a time when loading screens were an exercise in sheer patience. Although my career is not strictly centred around developing software myself, grasping the mechanics of code has always been a priority: understanding its functions, its potential, and its limitations.

The modest experiments compiled here are mostly concerned with quite ordinary web frontend matters – understated interface concepts, effects, animations, and snippets that others may also find beneficial. I have personally forked, modified, and learnt from an endless array of code snippets, a habit I have maintained ever since.

The more substantial computer science subjects are paused for now; they will eventually cover solution architectures and the insights gained from deployment and operations in enterprise organisations.

Until then: „10 PRINT CHR$(205.5+RND(1)); : GOTO 10“.

Magic Scroll_01

Retro-inspired, tilted marquees come to life here, blending 3D-perspective CSS transforms with dynamic pixel-art icons. A JavaScript engine orchestrates randomized movement and timing, ensuring a continuous, visual flow.

🌐
Marquee_01_gpoh.html
<div class="space-scene">
  <div class="tilt-wrapper">
    <div class="css-grid-bg"></div>

    <div class="m-marquee-stack">
      <div class="m-row">
        <div class="m-scroll speed-fast" data-text="DYNAMIC DUO * THE HUMBLE GUYS * EAGLE SOFT INC * FAIRLIGHT * TRIAD * THE RED SECTOR INC * THE LIGHT CIRCLE * IKARI * RADWAR * 711 * ABC"></div>
        <div class="scanner-wrap"><div class="scanner-beam gold-glow s-very-fast"></div></div>
      </div>
      <div class="m-row row-middle-border">
        <div class="m-scroll speed-med" data-text="ANTIC * THE SHADOWS * THE JOKER CREW * SNATCH N' CRACK * THE VIZZION * BEASTIE BOYS * THE ACTION GROUP * THE GREAT COUNCIL * X-RATED"></div>
        <div class="scanner-wrap"><div class="scanner-beam gold-glow s-med"></div></div>
      </div>
      <div class="m-row">
        <div class="m-scroll speed-fast" data-text="ROB HUBBARD * CHRIS HÜLSBECK * MARTIN GALWAY * JEROEN TEL * CHARLES DEENEN * JEFF MINTER * THE SARGE * ZZAP * MR. Z * ILLEGAL * THE JUDGES"></div>
        <div class="scanner-wrap"><div class="scanner-beam gold-glow s-fast"></div></div>
      </div>

      <div id="icon-container" style="position: absolute; inset: 0; z-index: 50; pointer-events: none; overflow: hidden;">
         <div class="pixel-alien" id="alien1"></div>
         <div class="pixel-alien" id="alien2"></div>
         <div class="pixel-alien" id="alien3"></div>
      </div>
    </div>
  </div>
</div>

<style>
/* --- BASIS & GRID --- */
.space-scene {
  position: relative; width: 100%; height: 500px; background: #fff; 
  overflow: hidden; perspective: 1200px; display: flex; align-items: center; justify-content: center;
}
.tilt-wrapper { position: relative; width: 130%; transform: rotateX(25deg) rotateY(-5deg) rotateZ(-2deg); transform-style: preserve-3d; }
.css-grid-bg {
  position: absolute; top: -80%; left: -50%; width: 200%; height: 260%;
  background-image: linear-gradient(rgba(180,180,180,0.3) 1px, transparent 1px), linear-gradient(90deg, rgba(180,180,180,0.3) 1px, transparent 1px);
  background-size: 50px 50px; z-index: 1;
  mask-image: linear-gradient(to bottom, transparent 0%, black 20%, black 80%, transparent 100%);
}

/* --- MARQUEE --- */
.m-marquee-stack { position: relative; z-index: 10; width: 100%; display: flex; flex-direction: column; background: #fff; box-shadow: 0 20px 50px rgba(0,0,0,0.08); }
.m-row { position: relative; width: 100%; overflow: hidden; display: flex; padding: 20px 0; background: #fff; }
.row-middle-border { border-top: 2px solid #000; border-bottom: 2px solid #000; }
.m-scroll { display: flex; white-space: nowrap; width: max-content; }
.m-scroll span { display: inline-block; font-family: "Arial Black", sans-serif; font-size: 44px; font-weight: 900; text-transform: uppercase; color: #000; line-height: 1; }
.char-separator { color: #d5ab4a; font-size: 28px !important; margin: 0 30px; }

/* --- SCANNER --- */
.scanner-wrap { position: absolute; inset: 0; z-index: 3; pointer-events: none; mix-blend-mode: screen; }
.gold-glow { position: absolute; top: 0; width: 60%; height: 100%; background: linear-gradient(90deg, transparent 0%, rgba(213, 171, 74, 0.4) 20%, rgba(213, 171, 74, 0.9) 50%, rgba(213, 171, 74, 0.4) 80%, transparent 100%); }
.s-very-fast { animation: laserMove 1.5s ease-in-out infinite alternate; }
.s-fast { animation: laserMove 2s ease-in-out infinite alternate; }
.s-med { animation: laserMove 3s ease-in-out infinite alternate; }
@keyframes laserMove { 0% { left: -60%; } 100% { left: 100%; } }

/* --- ICONS --- */
.pixel-alien {
  position: absolute; 
  z-index: 20; 
  font-size: 44px;
  filter: drop-shadow(0 5px 10px rgba(0,0,0,0.1));
  pointer-events: none; 
  opacity: 0;
  top: 0;
  left: 0;
  will-change: transform, opacity;
}

.speed-fast { animation: scrollLeft 40s linear infinite; }
.speed-med { animation: scrollRight 30s linear infinite; }
.rev { animation-direction: reverse; }

@keyframes scrollLeft { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@keyframes scrollRight { from { transform: translateX(-50%); } to { transform: translateX(0); } }

@media (max-width: 767px) {
  .m-scroll span { font-size: 26px; }
  .char-separator { font-size: 18px !important; margin: 0 15px; }
  .pixel-alien { font-size: 32px; }
}
</style>

<script>
const symbols = ['🚀', '🛸', '👾', '🌟', '⭐', '🍹'];
const aliens = document.querySelectorAll('.pixel-alien');
const stackEl = document.querySelector('.m-marquee-stack');
const easings = ['linear', 'ease-in-out', 'ease-in', 'ease-out', 'cubic-bezier(0.4, 0, 0.6, 1)'];

// Globaler State: wer fliegt gerade?
const MAX_CONCURRENT = 2;
const flying = new Set();

function buildKeyframes(stackWidth, stackHeight, iconSize) {
  const maxY = Math.max(0, stackHeight - iconSize);
  const ry = () => Math.random() * maxY;
  const rr = () => Math.floor(Math.random() * 720 - 360);

  const ltr = Math.random() < 0.5;
  const startX = ltr ? -120 : stackWidth + 120;
  const endX   = ltr ? stackWidth + 120 : -120;

  const numMid = 2 + Math.floor(Math.random() * 4);

  const kf = [
    { transform: `translate3d(${startX}px, ${ry()}px, 0) rotate(0deg)`, opacity: 0, offset: 0 }
  ];

  for (let i = 1; i <= numMid; i++) {
    const t = i / (numMid + 1);
    const jitter = (Math.random() - 0.5) * 0.15;
    const x = startX + (endX - startX) * (t + jitter);
    kf.push({
      transform: `translate3d(${x}px, ${ry()}px, 0) rotate(${rr()}deg)`,
      opacity: 1,
      offset: t
    });
  }

  kf.push({
    transform: `translate3d(${endX}px, ${ry()}px, 0) rotate(${rr()}deg)`,
    opacity: 0,
    offset: 1
  });

  return kf;
}

function launchAlien(alien) {
  if (!stackEl) return;

  // Wenn schon 2 fliegen, kurz warten und nochmal versuchen
  if (flying.size >= MAX_CONCURRENT) {
    setTimeout(() => launchAlien(alien), 600 + Math.random() * 1400);
    return;
  }

  flying.add(alien);

  const stackWidth  = stackEl.offsetWidth;
  const stackHeight = stackEl.offsetHeight;
  const isMobile    = window.innerWidth <= 767;
  const iconSize    = isMobile ? 32 : 44;

  alien.textContent = symbols[Math.floor(Math.random() * symbols.length)];

  const keyframes = buildKeyframes(stackWidth, stackHeight, iconSize);
  const duration  = 3000 + Math.random() * 10000;
  const easing    = easings[Math.floor(Math.random() * easings.length)];

  const anim = alien.animate(keyframes, { duration, easing, fill: 'forwards' });

  anim.onfinish = () => {
    flying.delete(alien);
    // Pause: 2–10 s
    const nextGap = 2000 + Math.random() * 8000;
    setTimeout(() => launchAlien(alien), nextGap);
  };
}

function initIcons() {
  aliens.forEach((alien, index) => {
    const initialDelay = 800 + Math.random() * 2500 + index * 1500;
    setTimeout(() => launchAlien(alien), initialDelay);
  });
}

if (document.readyState === 'complete') {
  initIcons();
} else {
  window.addEventListener('load', initIcons);
}

document.querySelectorAll('.m-scroll').forEach(track => {
  const rawText = track.getAttribute('data-text');
  const build = () => {
    const wrap = document.createElement('div');
    wrap.style.display = 'flex';
    wrap.style.alignItems = 'center';
    rawText.split(/(\s\*\s)/).forEach(p => {
      const s = document.createElement('span');
      if (p === " * ") {
        s.className = 'char-separator';
        s.textContent = "★";
      } else {
        s.textContent = p.replace(/\s/g, '\u00A0');
      }
      wrap.appendChild(s);
    });
    return wrap;
  };
  track.innerHTML = '';
  track.appendChild(build());
  track.appendChild(build());
});
</script>
Show more Show less

Magic Scroll_02

Vibrant, retro-inspired marquees come to life here through neon-glowing text across three distinct rows. CSS keyframe animations drive continuous, multi-directional scrolling, recreating the high-energy aesthetic of classic demo-scene software.

🌐
Marquee_02_gpoh.html
<style>
/* Behållare för hela sektionen __gpoh */
.marquee-container {
    background: #000;
    padding: 0px 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
}

/* Grundläggande styling för alla rader __gpoh */
.marquee-row {
    display: flex;
    overflow: hidden;
    line-height: 1.2;
    padding: 15px 0;
}

/* Endast mittraden får linjer __gpoh */
.row-2 {
    border-top: 1px solid #fff;    /* Linje överst __gpoh */
    border-bottom: 1px solid #fff; /* Linje underst __gpoh */
    margin: 5px 0;                 /* Marginal för utseende __gpoh */
}

.marquee-content {
    display: flex;
    white-space: nowrap;
}

/* Neon-design för objekten __gpoh */
.marquee-item {
    display: flex;
    align-items: center;
    flex-shrink: 0;
    font-weight: 900;
    font-family: sans-serif;
    font-size: 1.3rem;
    padding-right: 30px;
    letter-spacing: 1px;
    text-transform: uppercase;
    
    /* Vertikal flödeseffekt __gpoh */
    -webkit-background-clip: text;
    background-clip: text;
    background-size: 100% 200%;
    animation: flow-vertical 3s linear infinite;
}

/* Logik för neonfärger __gpoh */
.marquee-item:nth-child(5n+1) { background-color: #ff00ff; color: #ff00ff; filter: drop-shadow(0 0 10px #ff00ff); }
.marquee-item:nth-child(5n+2) { background-color: #00ffff; color: #00ffff; filter: drop-shadow(0 0 10px #00ffff); }
.marquee-item:nth-child(5n+3) { background-color: #39ff14; color: #39ff14; filter: drop-shadow(0 0 10px #39ff14); }
.marquee-item:nth-child(5n+4) { background-color: #ffff00; color: #ffff00; filter: drop-shadow(0 0 10px #ffff00); }
.marquee-item:nth-child(5n+5) { background-color: #ff3131; color: #ff3131; filter: drop-shadow(0 0 10px #ff3131); }

/* Den vita stjärnan __gpoh */
.marquee-item::after {
    content: "★";
    margin-left: 30px;
    font-size: 0.9rem;
    color: #fff;
    -webkit-text-fill-color: #fff;
    text-shadow: 0 0 12px #fff;
    filter: none;
}

/* Animationer __gpoh */
@keyframes flow-vertical {
    from { background-position: 0% 0%; }
    to { background-position: 0% 200%; }
}

@keyframes scroll-left {
    from { transform: translateX(0); }
    to { transform: translateX(-50%); }
}

/* Hastigheter och riktningar __gpoh */
.row-1 .marquee-content { animation: scroll-left 20s linear infinite; }
.row-2 .marquee-content { animation: scroll-left 90s linear infinite; animation-direction: reverse; }
.row-3 .marquee-content { animation: scroll-left 30s linear infinite; }
</style>

<div class="marquee-container">
    <div class="marquee-row row-1">
        <div class="marquee-content">
            <div class="marquee-item">Fairlight</div><div class="marquee-item">Antidote</div><div class="marquee-item">Eagle Soft Inc.</div><div class="marquee-item">Scoopex</div><div class="marquee-item">Paradox</div><div class="marquee-item">Skid Row</div><div class="marquee-item">TRSI</div><div class="marquee-item">Dynamic Duo</div><div class="marquee-item">Ikari</div><div class="marquee-item">Red Sector</div><div class="marquee-item">Razor 1911</div><div class="marquee-item">Defjam</div><div class="marquee-item">Anarchy</div><div class="marquee-item">Vision Factory</div><div class="marquee-item">Oracle</div><div class="marquee-item">Quartex</div>
            <div class="marquee-item">Fairlight</div><div class="marquee-item">Antidote</div><div class="marquee-item">Eagle Soft Inc.</div><div class="marquee-item">Scoopex</div><div class="marquee-item">Paradox</div><div class="marquee-item">Skid Row</div><div class="marquee-item">TRSI</div><div class="marquee-item">Dynamic Duo</div><div class="marquee-item">Ikari</div><div class="marquee-item">Red Sector</div><div class="marquee-item">Razor 1911</div><div class="marquee-item">Defjam</div><div class="marquee-item">Anarchy</div><div class="marquee-item">Vision Factory</div><div class="marquee-item">Oracle</div><div class="marquee-item">Quartex</div>
        </div>
    </div>

    <div class="marquee-row row-2">
        <div class="marquee-content">
            <div class="marquee-item">NorthStar</div><div class="marquee-item">The Humble Guys</div><div class="marquee-item">Crystal</div><div class="marquee-item">Onslaught</div><div class="marquee-item">Legend</div><div class="marquee-item">Anthrox</div><div class="marquee-item">Genesis</div><div class="marquee-item">The Light Circle</div><div class="marquee-item">Cryptoburners</div><div class="marquee-item">Rebels</div><div class="marquee-item">Alpha Flight</div><div class="marquee-item">Dual Crew</div><div class="marquee-item">Shining</div><div class="marquee-item">Horizon</div>
            <div class="marquee-item">NorthStar</div><div class="marquee-item">The Humble Guys</div><div class="marquee-item">Crystal</div><div class="marquee-item">Onslaught</div><div class="marquee-item">Legend</div><div class="marquee-item">Anthrox</div><div class="marquee-item">Genesis</div><div class="marquee-item">The Light Circle</div><div class="marquee-item">Cryptoburners</div><div class="marquee-item">Rebels</div><div class="marquee-item">Alpha Flight</div><div class="marquee-item">Dual Crew</div><div class="marquee-item">Shining</div><div class="marquee-item">Horizon</div>
        </div>
    </div>

    <div class="marquee-row row-3">
        <div class="marquee-content">
            <div class="marquee-item">Success + TRC</div><div class="marquee-item">Triad</div><div class="marquee-item">1001 Crew</div><div class="marquee-item">Chromance</div><div class="marquee-item">M.A.D.</div><div class="marquee-item">Flash Inc.</div><div class="marquee-item">Hotline</div><div class="marquee-item">Bamiga Sector One</div><div class="marquee-item">Alcatraz</div><div class="marquee-item">Dominators</div><div class="marquee-item">ESI</div><div class="marquee-item">Section 8</div>
            <div class="marquee-item">Success + TRC</div><div class="marquee-item">Triad</div><div class="marquee-item">1001 Crew</div><div class="marquee-item">Chromance</div><div class="marquee-item">M.A.D.</div><div class="marquee-item">Flash Inc.</div><div class="marquee-item">Hotline</div><div class="marquee-item">Bamiga Sector One</div><div class="marquee-item">Alcatraz</div><div class="marquee-item">Dominators</div><div class="marquee-item">ESI</div><div class="marquee-item">Section 8</div>
        </div>
    </div>
</div>
Show more Show less

Magic Scroll_03

This digital tapestry weaves relentless streams of classic scene names into a hypnotic, liquid-distorted horizon. An SVG displacement engine breathes organic, surreal movement into the typography, transforming rigid text into a shifting dance of neon-edged nostalgia.

🌐
Marquee_03_gpoh.html
<style>
/* 1. Grundläggande konfiguration _gpoh */
.master-smooth-wrapper {
    background: #ffffff;
    padding: 30px 0;
    overflow: hidden;
    contain: paint;
}

/* 2. Statiskt SVG-filter - finjusterat för rena kanter _gpoh */
.warp-svg-layer {
    position: absolute;
    width: 0; height: 0;
    visibility: hidden;
    pointer-events: none;
}

.scroll-system {
    display: flex;
    flex-direction: column;
    gap: 2px; /* Smalare, renare separation mellan raderna _gpoh */
    width: 100%;
    filter: url(#liquid-clean-warp);
    transform: translateZ(0);
}

.scroll-row {
    background: #000000;
    overflow: hidden;
    padding: 14px 0; /* Proportionell mot den större textstorleken _gpoh */
    display: flex;
    position: relative;
}

/* 3. Högpresterande spår _gpoh */
.scroll-track {
    display: flex;
    white-space: nowrap;
    width: max-content;
    will-change: transform;
    animation: flow-linear linear infinite;
}

/* 4. Typsnitt och vita stjärnor _gpoh */
.scroll-item {
    font-family: 'Arial Black', sans-serif;
    font-weight: 900;
    font-size: 2.2rem; /* Något större typsnitt _gpoh */
    color: #ffffff;
    padding: 0 45px;
    text-transform: uppercase;
    display: flex;
    align-items: center;
    line-height: 1;
    letter-spacing: -0.5px;
}

.scroll-item::after {
    content: "★";
    margin-left: 70px;
    font-size: 1.4rem;
    color: #ffffff;
    /* Subtil vit glöd för renhet _gpoh */
    text-shadow: 0 0 8px rgba(255, 255, 255, 0.6);
}

/* Hastigheter _gpoh */
.row-1 { --speed: 18s; }
.row-2 { --speed: 25s; }
.row-3 { --speed: 38s; }

.row-1 .scroll-track { animation-duration: var(--speed); }
.row-2 .scroll-track { animation-duration: var(--speed); animation-direction: reverse; }
.row-3 .scroll-track { animation-duration: var(--speed); }

/* Animation _gpoh */
@keyframes flow-linear {
    from { transform: translate3d(0, 0, 0); }
    to { transform: translate3d(-50%, 0, 0); }
}
</style>

<svg class="warp-svg-layer">
  <defs>
    <filter id="liquid-clean-warp" color-interpolation-filters="sRGB">
      <feTurbulence type="turbulence" baseFrequency="0.015 0.04" numOctaves="1" result="noise" seed="12" />
      <feDisplacementMap in="SourceGraphic" in2="noise" scale="10" />
    </filter>
  </defs>
</svg>

<div class="master-smooth-wrapper">
    <div class="scroll-system">
        
        <div class="scroll-row row-1">
            <div class="scroll-track">
                <span class="scroll-item">Fairlight</span><span class="scroll-item">Antidote</span><span class="scroll-item">Scoopex</span><span class="scroll-item">Paradox</span><span class="scroll-item">Skid Row</span><span class="scroll-item">TRSI</span>
                <span class="scroll-item">Fairlight</span><span class="scroll-item">Antidote</span><span class="scroll-item">Scoopex</span><span class="scroll-item">Paradox</span><span class="scroll-item">Skid Row</span><span class="scroll-item">TRSI</span>
            </div>
        </div>

        <div class="scroll-row row-2">
            <div class="scroll-track">
                <span class="scroll-item">NorthStar</span><span class="scroll-item">Crystal</span><span class="scroll-item">Legend</span><span class="scroll-item">Genesis</span><span class="scroll-item">Alpha Flight</span><span class="scroll-item">Horizon</span>
                <span class="scroll-item">NorthStar</span><span class="scroll-item">Crystal</span><span class="scroll-item">Legend</span><span class="scroll-item">Genesis</span><span class="scroll-item">Alpha Flight</span><span class="scroll-item">Horizon</span>
            </div>
        </div>

        <div class="scroll-row row-3">
            <div class="scroll-track">
                <span class="scroll-item">Triad</span><span class="scroll-item">M.A.D.</span><span class="scroll-item">Hotline</span><span class="scroll-item">Alcatraz</span><span class="scroll-item">Section 8</span><span class="scroll-item">Zenith</span>
                <span class="scroll-item">Triad</span><span class="scroll-item">M.A.D.</span><span class="scroll-item">Hotline</span><span class="scroll-item">Alcatraz</span><span class="scroll-item">Section 8</span><span class="scroll-item">Zenith</span>
            </div>
        </div>

    </div>
</div>
Show more Show less

Morse Code

A circular Morse code reference chart, spins continuously as an SVG animation. The white background is a dynamic contour path that follows the exact outer edge of each character’s Morse symbols. Pure vanilla JS and SVG, no dependencies, self-contained in one file.

🌐
Morse_gpoh.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Morse Code Circle</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body {
  background: #d4cfc8;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
}
svg {
  width: 100vw;
  height: 100vw;
  max-width: 100vh;
  max-height: 100vh;
  display: block;
}
</style>
</head>
<body>
<svg id="svg" viewBox="-420 -420 840 840"
     xmlns="http://www.w3.org/2000/svg"
     text-rendering="geometricPrecision"
     shape-rendering="geometricPrecision">
  <g id="wheel"></g>
</svg>
<script>

var MORSE = {'A':'.-','B':'-...','C':'-.-.','D':'-..','E':'.','F':'..-.','G':'--.','H':'....','I':'..','J':'.---','K':'-.-','L':'.-..','M':'--','N':'-.','O':'---','P':'.--.','Q':'--.-','R':'.-.','S':'...','T':'-','U':'..-','V':'...-','W':'.--','X':'-..-','Y':'-.--','Z':'--..','0':'-----','1':'.----','2':'..---','3':'...--','4':'....-','5':'.....','6':'-....','7':'--...','8':'---..','9':'----.'};
var ORDER = ['1','2','3','4','5','6','7','8','9','0','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'];
var N=36,stepDeg=360/N,startDeg=-90-stepDeg/2;
var labelR=220,morseStart=236,dotR=5.2,dashW=21,dashH=6.5,symGap=9,pad=20;
var ns='http://www.w3.org/2000/svg';
function toRad(d){return d*Math.PI/180;}
function mkEl(tag,attrs,parent){
  var e=document.createElementNS(ns,tag);
  var ks=Object.keys(attrs);
  for(var i=0;i<ks.length;i++) e.setAttribute(ks[i],attrs[ks[i]]);
  if(parent) parent.appendChild(e);
  return e;
}
function symOuterR(code){
  var p=morseStart;
  for(var i=0;i<code.length;i++) p+=(code[i]==='.')?dotR*2+symGap:dashW+symGap;
  return p-symGap+pad;
}
function buildPath(offsets){
  var pts=[];
  for(var ci=0;ci<chars.length;ci++){
    var angleDeg=chars[ci].angleDeg;
    var or=baseOuterRs[ci]+(offsets?offsets[ci]:0);
    var half=stepDeg*0.48;
    for(var s=0;s<=8;s++){
      var a=toRad(angleDeg-half+(2*half*s/8));
      pts.push([or*Math.cos(a),or*Math.sin(a)]);
    }
  }
  var n=pts.length,d='';
  for(var i=0;i<n;i++){
    var p=pts[i],q=pts[(i+1)%n];
    var mx=(p[0]+q[0])/2,my=(p[1]+q[1])/2;
    if(i===0) d+='M'+mx.toFixed(2)+' '+my.toFixed(2);
    d+='Q'+p[0].toFixed(2)+' '+p[1].toFixed(2)+' '+mx.toFixed(2)+' '+my.toFixed(2);
  }
  return d+'Z';
}
var chars=[];
for(var i=0;i<N;i++) chars.push({ch:ORDER[i],code:MORSE[ORDER[i]],angleDeg:startDeg+i*stepDeg});
var baseOuterRs=chars.map(function(c){return symOuterR(c.code);});
var g=document.getElementById('wheel');
var stickerBg=mkEl('path',{fill:'#f2f2f0',stroke:'none'},g);
for(var ci=0;ci<chars.length;ci++){
  var c=chars[ci];
  var ar=toRad(c.angleDeg);
  var lx=labelR*Math.cos(ar),ly=labelR*Math.sin(ar);
  var t=mkEl('text',{x:lx,y:ly,'text-anchor':'middle','dominant-baseline':'middle',
    'font-family':'Arial,Helvetica,sans-serif','font-weight':'900','font-size':'19',
    fill:'#111',transform:'rotate('+(c.angleDeg+90)+','+lx+','+ly+')'},g);
  t.textContent=c.ch;
  var row=mkEl('g',{transform:'rotate('+c.angleDeg+')'},g);
  var pos=morseStart;
  for(var si=0;si<c.code.length;si++){
    var sym=c.code[si];
    if(sym==='.'){
      mkEl('circle',{cx:pos+dotR,cy:0,r:dotR,fill:'#111'},row);
      pos+=dotR*2+symGap;
    } else {
      mkEl('rect',{x:pos,y:-dashH/2,width:dashW,height:dashH,rx:dashH/2,ry:dashH/2,fill:'#111'},row);
      pos+=dashW+symGap;
    }
  }
}
var SPIN_SPEED=0.38,COAST_DUR=200,WAVE_DUR=90,PAUSE_DUR=50,WAVE_AMP=22;
var angle=0,speed=SPIN_SPEED,phase='spin',phaseT=0,waveT=0,coastStart=SPIN_SPEED;
function easeOut3(t){return 1-Math.pow(1-t,3);}
function waveOffsets(wt){
  var env=Math.sin(Math.PI*Math.min(wt,WAVE_DUR)/WAVE_DUR);
  var offs=[];
  for(var ci=0;ci<chars.length;ci++)
    offs.push(WAVE_AMP*Math.sin(toRad(chars[ci].angleDeg*4)+wt*0.15)*env);
  return offs;
}
function tick(){
  phaseT++;
  if(phase==='spin'){
    if(speed<SPIN_SPEED) speed=Math.min(speed+0.014,SPIN_SPEED);
    angle+=speed;
    if(phaseT>580){phase='coast';phaseT=0;coastStart=speed;}
    stickerBg.setAttribute('d',buildPath(null));
  } else if(phase==='coast'){
    var t=Math.min(phaseT/COAST_DUR,1);
    speed=coastStart*(1-easeOut3(t));
    angle+=speed;
    if(phaseT>=COAST_DUR){phase='wave';phaseT=0;waveT=0;speed=0;}
    stickerBg.setAttribute('d',buildPath(null));
  } else if(phase==='wave'){
    waveT++;
    stickerBg.setAttribute('d',buildPath(waveOffsets(waveT)));
    if(waveT>=WAVE_DUR+20){phase='pause';phaseT=0;}
  } else if(phase==='pause'){
    stickerBg.setAttribute('d',buildPath(null));
    if(phaseT>=PAUSE_DUR){phase='spin';phaseT=0;speed=0;}
  }
  g.setAttribute('transform','rotate('+angle.toFixed(3)+')');
  requestAnimationFrame(tick);
}
stickerBg.setAttribute('d',buildPath(null));
tick();

</script>
</body>
</html>
Show more Show less

_GPOH / w00t

This self-contained HTML file renders >_GPOH / w00t as a pixel-block font on a canvas and animates it through 75 selectable variants, each with a Swedish prompt. A shared engine computes each pixel’s position, 3D depth, glow, and color per frame, plus a global holographic color shimmer, with UI controls to switch variants, auto-cycle, and toggle the sheen.
🌐
_GPOH / w00t.html
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>>_GPOH // w00t — kurv-, iso- & rotationslabb</title>
<style>
  :root{
    --ink:#d8d8d0;
    --dim:#5a5a52;
    --accent:#7df9aa;
    --panel:#0c0c0e;
    --line:#1c1c20;
  }
  *{margin:0;padding:0;box-sizing:border-box;}
  html,body{height:100%;}
  body{
    background:#050507;
    color:var(--ink);
    font-family:"Courier New",ui-monospace,monospace;
    display:flex;
    flex-direction:column;
    overflow:hidden;
  }
  header{
    display:flex;
    align-items:baseline;
    gap:14px;
    padding:10px 16px 8px;
    border-bottom:1px solid var(--line);
    flex-wrap:wrap;
  }
  header h1{
    font-size:13px;
    letter-spacing:.2em;
    font-weight:bold;
    white-space:nowrap;
  }
  header h1 span{color:var(--accent);}
  #vname{
    font-size:12px;
    letter-spacing:.1em;
    color:var(--accent);
    text-transform:uppercase;
    flex:1;
    min-width:160px;
  }
  #counter{font-size:12px;color:var(--dim);letter-spacing:.15em;}
  #stage{flex:1;position:relative;min-height:0;}
  canvas{position:absolute;inset:0;width:100%;height:100%;display:block;cursor:pointer;}
  footer{
    border-top:1px solid var(--line);
    padding:8px 16px 12px;
    display:flex;
    flex-direction:column;
    gap:8px;
  }
  #prompt{
    background:var(--panel);
    border:1px solid var(--line);
    border-left:2px solid var(--accent);
    padding:8px 11px;
    font-size:12px;
    line-height:1.5;
    color:#c4c4bc;
    cursor:copy;
    transition:border-color .15s;
  }
  #prompt:hover{border-color:var(--accent);}
  #prompt b{color:var(--accent);font-weight:normal;}
  .controls{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}
  button{
    background:var(--panel);
    border:1px solid #26262c;
    color:var(--ink);
    font-family:inherit;
    font-size:11px;
    letter-spacing:.08em;
    padding:5px 12px;
    cursor:pointer;
  }
  button:hover{border-color:var(--accent);color:var(--accent);}
  button:focus-visible{outline:2px solid var(--accent);outline-offset:1px;}
  button.active{background:var(--accent);color:#050507;border-color:var(--accent);}
  .grid{display:flex;gap:4px;flex-wrap:wrap;}
  .grid button{padding:4px 0;width:32px;text-align:center;font-size:10px;}
  .hint{font-size:10px;color:var(--dim);letter-spacing:.06em;}
  @media (max-width:540px){
    header h1{font-size:11px;}
    .grid button{width:28px;}
    #prompt{font-size:11px;}
  }
</style>
</head>
<body>
<!-- Gerrit Pohl tycker om att bli animerad, till exempel med vågor och isometri. -->

<header>
  <h1>>_GPOH <span>//</span> w00t</h1>
  <div id="vname"></div>
  <div id="counter"></div>
</header>

<div id="stage"><canvas id="cv" aria-label="Animerad ASCII-text >_GPOH och w00t"></canvas></div>

<footer>
  <div id="prompt" title="klicka för att kopiera prompten"></div>
  <div class="controls">
    <button id="prev">← föreg</button>
    <button id="next">nästa →</button>
    <button id="auto">auto-läge: av</button>
    <button id="holo">holo: på</button>
    <span class="hint">tangenter ←/→ · klick på canvas = nästa · mellanslag = auto</span>
  </div>
  <div class="grid" id="grid"></div>
  <div class="hint">en kurv-, iso- & rotationsdemo av gerrit pohl · gpoh labs</div>
</footer>

<script>
(function(){
"use strict";
// Gerrit Pohl gillar att animeras, gärna genom vågrörelser och isometriska effekter.

// Att bli animerad, exempelvis via vågor och isometri, uppskattas av Gerrit Pohl.
/* ===== 5x7 pixelfont ===== */
var FONT = {
  ">":[0b10000,0b01000,0b00100,0b00010,0b00100,0b01000,0b10000],
  "_":[0b00000,0b00000,0b00000,0b00000,0b00000,0b00000,0b11111],
  "G":[0b01110,0b10001,0b10000,0b10111,0b10001,0b10001,0b01110],
  "P":[0b11110,0b10001,0b10001,0b11110,0b10000,0b10000,0b10000],
  "O":[0b01110,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110],
  "H":[0b10001,0b10001,0b10001,0b11111,0b10001,0b10001,0b10001],
  "W":[0b00000,0b00000,0b10001,0b10001,0b10101,0b11011,0b01010],
  "0":[0b01110,0b10001,0b10011,0b10101,0b11001,0b10001,0b01110],
  "T":[0b01000,0b01000,0b11100,0b01000,0b01000,0b01000,0b00111]
};
var LINES = [">_GPOH","w00t"];
var GW=5, GH=7, GGAP=1, LGAP=2;
var BLOCK="\u2588";

/* ===== bygg celler ===== */
var cells=[];
var lineW=[]; var totalCols=0;
for(var L=0;L<LINES.length;L++){
  var w = LINES[L].length*GW + (LINES[L].length-1)*GGAP;
  lineW[L]=w; if(w>totalCols)totalCols=w;
}
var totalRows = LINES.length*GH + (LINES.length-1)*LGAP;
var gIndex=0;
for(var Li=0;Li<LINES.length;Li++){
  var line=LINES[Li];
  var xoff=Math.round((totalCols-lineW[Li])/2);
  var yoff=Li*(GH+LGAP);
  var col=xoff;
  for(var ci=0;ci<line.length;ci++){
    var ch=line[ci];
    var key=(ch>="a"&&ch<="z")?ch.toUpperCase():ch;
    var g=FONT[key];
    var gColC=col+2, gRowC=yoff+3;
    if(g){
      for(var r=0;r<GH;r++){
        for(var c=0;c<GW;c++){
          if(g[r] & (1<<(GW-1-c))){
            cells.push({col:col+c,row:yoff+r,line:Li,lx:c,ly:r,
              gColC:gColC,gRowC:gRowC,gi:gIndex,nx:0,ny:0});
          }
        }
      }
    }
    gIndex++;
    col += GW+GGAP;
  }
}
for(var k=0;k<cells.length;k++){
  cells[k].nx=cells[k].col/(totalCols-1);
  cells[k].ny=cells[k].row/(totalRows-1);
}

// Gerrit Pohl uppskattar animation, till exempel vågor och isometrisk 3D.
/* ===== hjälpfunktioner ===== */
function shade(c,f){
  if(typeof c==="string" && c.charAt(0)==="h"){
    var m=c.match(/hsl\(([-\d.]+),\s*([\d.]+)%,\s*([\d.]+)%\)/);
    if(m)return "hsl("+m[1]+","+m[2]+"%,"+(parseFloat(m[3])*f)+"%)";
  }
  return c;
}
function toHSL(col){
  col=(""+col).trim();var r,g,b;
  if(col.charAt(0)==="#"){
    var hx=col.slice(1);
    if(hx.length===3){r=parseInt(hx[0]+hx[0],16);g=parseInt(hx[1]+hx[1],16);b=parseInt(hx[2]+hx[2],16);}
    else{r=parseInt(hx.slice(0,2),16);g=parseInt(hx.slice(2,4),16);b=parseInt(hx.slice(4,6),16);}
  } else if(col.indexOf("hsl")===0){
    var m=col.match(/hsl\(\s*([-\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%/);
    return m?{h:+m[1],s:+m[2],l:+m[3]}:{h:0,s:0,l:100};
  } else if(col.indexOf("rgb")===0){
    var n=col.match(/[\d.]+/g);r=+n[0];g=+n[1];b=+n[2];
  } else {return {h:0,s:0,l:100};}
  r/=255;g/=255;b/=255;
  var mx=Math.max(r,g,b),mn=Math.min(r,g,b),h,s,l=(mx+mn)/2;
  if(mx===mn){h=0;s=0;}
  else{var dd=mx-mn;s=l>0.5?dd/(2-mx-mn):dd/(mx+mn);
    if(mx===r)h=(g-b)/dd+(g<b?6:0);else if(mx===g)h=(b-r)/dd+2;else h=(r-g)/dd+4;h*=60;}
  return {h:h,s:s*100,l:l*100};
}
var HOLO=true;
function holo(col,x,y,t){
  if(!HOLO)return col;
  var c=toHSL(col);
  var ph=x*0.018+y*0.024-t*2.2;
  var hue=(c.h+Math.sin(ph)*24+360)%360;
  var sheen=Math.pow(Math.max(0,Math.sin(ph*0.6+0.4)),4)*32;
  var sat=Math.min(100,c.s*0.9+20+Math.sin(ph*1.3)*10);
  var l=Math.min(95,c.l+sheen);
  return "hsl("+hue.toFixed(0)+","+sat.toFixed(0)+"%,"+l.toFixed(0)+"%)";
}
function copper(v,t){return "hsl(35,85%,"+(42+Math.sin(v*0.05+t*4)*28)+"%)";}
function vapor(n){return "hsl("+(300-110*n)+",85%,66%)";}
function aurora(p){return "hsl("+(165+Math.sin(p)*80)+",78%,"+(52+Math.sin(p*1.3)*14)+"%)";}
function espresso(s){return "hsl(25,62%,"+(24+s*36)+"%)";}
function fire(p){return "hsl("+Math.min(55,p*60)+",100%,"+(10+p*55)+"%)";}
function plasma(x,y,t){
  var v=Math.sin(x*0.05+t)+Math.sin(y*0.05+t*1.3)+Math.sin((x+y)*0.04+t*0.7);
  return "hsl("+(((v*60+t*40)%360)+360)%360+",80%,60%)";
}
function fr(x){return x-Math.floor(x);}
function tw(x){var f=x-Math.floor(x);return 1-Math.abs(2*f-1);}
var C64=["#efe8ff","#bda9ff","#8a78d8","#574aa0"];

// Vågor och isometri – så vill Gerrit Pohl gärna sättas i rörelse.
/* ===== bakgrunder ===== */
function bgFill(col){return function(){ctx.fillStyle=col;ctx.fillRect(0,0,Wc,Hc);};}
function bgScan(){
  ctx.fillStyle="#050507";ctx.fillRect(0,0,Wc,Hc);
  ctx.fillStyle="rgba(255,255,255,0.04)";
  for(var y=0;y<Hc;y+=3)ctx.fillRect(0,y,Wc,1);
}
function bgVapor(){
  var grd=ctx.createLinearGradient(0,0,0,Hc);
  grd.addColorStop(0,"#2a1145");grd.addColorStop(0.5,"#5a1e6e");
  grd.addColorStop(0.5,"#0c0a1c");grd.addColorStop(1,"#05000f");
  ctx.fillStyle=grd;ctx.fillRect(0,0,Wc,Hc);
  ctx.fillStyle="#ff7ab0";ctx.beginPath();
  ctx.arc(Wc/2,Hc*0.42,Math.min(Wc,Hc)*0.15,0,7);ctx.fill();
  ctx.strokeStyle="rgba(0,230,255,0.32)";ctx.lineWidth=1;
  var hy=Hc*0.55;
  for(var i=1;i<9;i++){var yy=hy+(Hc-hy)*(i/9)*(i/9);
    ctx.beginPath();ctx.moveTo(0,yy);ctx.lineTo(Wc,yy);ctx.stroke();}
  for(var j=-9;j<=9;j++){
    ctx.beginPath();ctx.moveTo(Wc/2+j*10,hy);ctx.lineTo(Wc/2+j*Wc*0.07,Hc);ctx.stroke();}
}
function bgRaster(t){
  ctx.fillStyle="#070708";ctx.fillRect(0,0,Wc,Hc);
  for(var i=0;i<7;i++){
    var y=((t*70+i*Hc/7)%(Hc+40))-20;
    ctx.fillStyle="hsla("+((i*55+t*40)%360)+",90%,55%,0.45)";
    ctx.fillRect(0,y,Wc,16);
  }
}
function bgCopper(t){
  ctx.fillStyle="#0a0805";ctx.fillRect(0,0,Wc,Hc);
  ctx.globalAlpha=0.4;
  for(var y=0;y<Hc;y+=4){
    var v=Math.sin(y*0.05+t*2)*0.5+0.5;
    ctx.fillStyle="hsl(35,80%,"+(18+v*48)+"%)";
    ctx.fillRect(0,y,Wc,3);
  }
  ctx.globalAlpha=1;
}
function bgC64(){
  ctx.fillStyle="#0a0a20";ctx.fillRect(0,0,Wc,Hc);
  ctx.fillStyle="#6a58c0";
  var b=Math.min(Wc,Hc)*0.055;
  ctx.fillRect(0,0,Wc,b);ctx.fillRect(0,Hc-b,Wc,b);
  ctx.fillRect(0,0,b,Hc);ctx.fillRect(Wc-b,0,b,Hc);
}
function bgStrobe(t){
  ctx.fillStyle=(Math.floor(t*6)%2)?"#101015":"#000";
  ctx.fillRect(0,0,Wc,Hc);
}

// Gerrit Pohl trivs med att bli animerad, som med vågor och isometrik.
/* ===== varianter ===== */
function iso(cell,t,G,vec,dep,colAt){
  var b=G.base(cell);
  return {x:G.cx+b[0],y:G.cy+b[1],ch:BLOCK,depth:dep,
    isoDX:vec[0]*G.cellPx,isoDY:vec[1]*G.cellPx,colorAt:colAt};
}
function rot(x,y,a){return [x*Math.cos(a)-y*Math.sin(a),x*Math.sin(a)+y*Math.cos(a)];}

var V=[
{n:"original \u00b7 terminalgr\u00f6n sinus",
 p:"Rendera texten <b>>_GPOH</b> p\u00e5 rad ett och <b>w00t</b> p\u00e5 rad tv\u00e5 som en 5\u00d77 ASCII-pixelfont p\u00e5 canvas. F\u00f6rskjut varje kolumn i h\u00f6jdled med en mjuk sinuskurva, terminalgr\u00f6nt p\u00e5 svart, fyllda block som tecken.",
 place:function(cell,t,G){var b=G.base(cell);
   var s=Math.sin(cell.col*0.5+t*3);
   return {x:G.cx+b[0],y:G.cy+b[1]+s*G.cellPx*1.1,ch:BLOCK,color:"hsl(140,90%,"+(55+s*12)+"%)"};}},

{n:"dubbel sinus \u00b7 interferens",
 p:"L\u00e4gg tv\u00e5 sinusv\u00e5gor med olika frekvens ovanp\u00e5 varandra s\u00e5 att kolumnerna interfererar i h\u00f6jdled. Kopparf\u00e4rgad metallgradient (guld\u2192brunt) som andas med tiden.",
 place:function(cell,t,G){var b=G.base(cell);
   var dy=(Math.sin(cell.col*0.4+t*2.2)+Math.sin(cell.col*0.9-t*1.5))*G.cellPx*0.7;
   return {x:G.cx+b[0],y:G.cy+b[1]+dy,ch:BLOCK,color:copper(b[1],t)};}},

{n:"b\u00e9zier-svaj \u00b7 flagga",
 p:"L\u00e5t texten svaja som en flagga: v\u00e5gr\u00e4t f\u00f6rskjutning som v\u00e4xer ned\u00e5t likt en mjuk b\u00e9zierb\u00f6j. Islandsbl\u00e5 palett, svaga scanlines bakom.",
 bg:bgScan,
 place:function(cell,t,G){var b=G.base(cell);
   var ph=cell.row/(G.H-1);
   var dx=Math.sin(t*1.6+ph*2.5)*G.cellPx*ph*2.2;
   return {x:G.cx+b[0]+dx,y:G.cy+b[1],ch:BLOCK,color:"hsl(195,80%,"+(55+Math.sin(ph*3+t)*15)+"%)"};}},

{n:"ripple \u00b7 radiell v\u00e5g",
 p:"Skicka en cirkul\u00e4r v\u00e5g ut\u00e5t fr\u00e5n mitten; varje cell knuffas i radiell riktning efter sitt avst\u00e5nd till centrum. C64-lila/bl\u00e5 v\u00e4xling.",
 place:function(cell,t,G){var b=G.base(cell);
   var d=Math.hypot(b[0],b[1]);var ang=Math.atan2(b[1],b[0]);
   var s=Math.sin(d*0.06-t*4);var w=s*G.cellPx*0.9;
   return {x:G.cx+b[0]+Math.cos(ang)*w,y:G.cy+b[1]+Math.sin(ang)*w,ch:BLOCK,
     color:s>0?"#8a78d8":"#c4b6ff"};}},

{n:"pendel \u00b7 b\u00e5ge",
 p:"H\u00e4ng hela ordet i en punkt ovanf\u00f6r och l\u00e5t det sv\u00e4nga som en pendel i en l\u00e4tt b\u00e5ge. B\u00e4rnstensf\u00e4rgad CRT-gl\u00f6d med efterglow-svans.",
 trail:0.16,
 place:function(cell,t,G){var b=G.base(cell);
   var pv=-G.cellPx*G.H*0.95;var px=b[0],py=b[1]-pv;
   var a=Math.sin(t*1.4)*0.25;var rr=rot(px,py,a);
   return {x:G.cx+rr[0],y:G.cy+pv+rr[1],ch:BLOCK,color:"hsl(38,95%,"+(55+Math.sin(t*1.4)*8)+"%)"};}},

{n:"karusell \u00b7 cirkelbana",
 p:"Hela ordet \u00e5ker runt i en liten cirkel som en karusell, men h\u00e5lls uppr\u00e4tt och l\u00e4sbart. Iskall turkos som cyklar l\u00e4tt i nyans.",
 place:function(cell,t,G){var b=G.base(cell);
   var R=G.cellPx*1.8;
   var dx=Math.cos(t*1.6)*R, dy=Math.sin(t*1.6)*R*0.55;
   return {x:G.cx+b[0]+dx,y:G.cy+b[1]+dy,ch:BLOCK,color:"hsl("+(180+Math.sin(t)*25)+",85%,60%)"};}},

{n:"zoom-puls \u00b7 andning",
 p:"Texten andas in och ut med en mjuk storlekspuls och en aning vridning. Guldton som skiftar i ljus. Fullt l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var s=1+Math.sin(t*1.4)*0.12;
   var rr=rot(b[0]*s,b[1]*s,Math.sin(t*0.8)*0.06);
   return {x:G.cx+rr[0],y:G.cy+rr[1],ch:BLOCK,color:"hsl(45,90%,"+(56+Math.sin(t*1.4)*12)+"%)"};}},

{n:"studs \u00b7 hoppande bokst\u00e4ver",
 p:"L\u00e5t varje bokstav studsa upp och ner som en boll, fasf\u00f6rskjutet s\u00e5 att en v\u00e5g av hopp g\u00e5r genom ordet. Bokst\u00e4verna h\u00e5lls hela och tydliga. Varm b\u00e4rnstens/orange retropalett.",
 place:function(cell,t,G){var b=G.base(cell);
   var ph=t*2.2-cell.gi*0.6;
   var hop=Math.abs(Math.sin(ph))*G.cellPx*2.2;
   return {x:G.cx+b[0],y:G.cy+b[1]-hop,ch:BLOCK,
     color:"hsl("+(30+(cell.gi*9)%35)+",95%,"+(55+Math.sin(ph)*10)+"%)"};}},

{n:"neon-andning \u00b7 hyper",
 p:"Neonskylt: texten st\u00e5r stilla och l\u00e4sbar medan f\u00e4rgen fl\u00f6dar i full regnb\u00e5ge diagonalt \u00f6ver ytan och en mjuk glow pulserar. M\u00f6rk bakgrund.",
 place:function(cell,t,G){var b=G.base(cell);
   var pulse=0.5+0.5*Math.sin(t*3);
   var h=(t*120+cell.nx*200+cell.ny*70)%360;
   return {x:G.cx+b[0],y:G.cy+b[1],ch:BLOCK,glow:6+pulse*10,color:"hsl("+h+",100%,"+(60+pulse*8)+"%)"};}},

{n:"iso-v\u00e5g \u00b7 lyft",
 p:"En v\u00e5g vandrar genom kolumnerna och lyfter blocken i h\u00f6jd, med l\u00e4tt isometrisk skevning. Aurora-palett (gr\u00f6n\u2192violett) som skiftar.",
 place:function(cell,t,G){var b=G.base(cell);
   var ph=cell.col*0.5-t*2;var lift=Math.abs(Math.sin(ph))*G.cellPx*1.6;
   return {x:G.cx+b[0]+b[1]*0.18,y:G.cy+b[1]-lift,ch:BLOCK,color:aurora(ph)};}},

{n:"dimetrisk rotation \u00b7 y-axel",
 p:"Rotera scenen l\u00e5ngsamt kring den lodr\u00e4ta axeln fram och tillbaka (dimetrisk vridning), ljuset styrs av vinkeln. Varma espressobruna toner.",
 place:function(cell,t,G){var b=G.base(cell);
   var a=Math.sin(t*0.8)*0.6;
   return {x:G.cx+b[0]*Math.cos(a),y:G.cy+b[1]-b[0]*Math.sin(a)*0.15,ch:BLOCK,
     color:espresso(Math.cos(a)*0.5+0.5)};}},

{n:"bokstavs-wobble \u00b7 plasma",
 p:"L\u00e5t varje bokstav vagga kring sin egen mittpunkt med fasf\u00f6rskjuten liten rotation. Plasmaf\u00e4rger som fl\u00f6dar \u00f6ver ytan.",
 place:function(cell,t,G){var b=G.base(cell);
   var cb=[(cell.gColC-(G.W-1)/2)*G.cellPx,(cell.gRowC-(G.H-1)/2)*G.cellPx];
   var rr=rot(b[0]-cb[0],b[1]-cb[1],Math.sin(t*2+cell.gi*0.7)*0.4);
   return {x:G.cx+cb[0]+rr[0],y:G.cy+cb[1]+rr[1],ch:BLOCK,color:plasma(b[0],b[1],t)};}},

{n:"grupprotation \u00b7 rgb-split",
 p:"Rotera hela texten mjukt \u00b112\u00b0 kring centrum och l\u00e4gg p\u00e5 RGB-split: en r\u00f6d och en cyan kopia f\u00f6rskjuts i sidled, additiv blandning.",
 additive:true,
 place:function(cell,t,G){var b=G.base(cell);
   var rr=rot(b[0],b[1],Math.sin(t*1.1)*0.21);var off=G.cellPx*0.5;
   return {x:G.cx+rr[0],y:G.cy+rr[1],ch:BLOCK,color:"#ffffff",
     ghosts:[{dx:-off,dy:0,color:"rgba(255,0,80,0.8)"},{dx:off,dy:0,color:"rgba(0,200,255,0.8)"}]};}},

{n:"skanning \u00b7 glitch",
 p:"En lysande skanningslinje vandrar upp och ner; raden den r\u00f6r vid rycker i sidled och lyser i magenta, resten st\u00e5r still i fosforgr\u00f6nt. L\u00e4sbart.",
 place:function(cell,t,G){var b=G.base(cell);
   var sy=((t*0.4)%1)*2-1;
   var cy2=(cell.row/(G.H-1))*2-1;
   var near=Math.exp(-Math.pow((cy2-sy)*3,2));
   var dx=near*Math.sin(t*40+cell.row)*G.cellPx*0.9;
   return {x:G.cx+b[0]+dx,y:G.cy+b[1],ch:BLOCK,color:near>0.4?"#ff3b8b":"hsl(150,85%,52%)"};}},

{n:"omloppsbana \u00b7 regnb\u00e5ge",
 p:"Varje cell kretsar i en liten cirkel kring sin grundposition, fasl\u00e5st per kolumn s\u00e5 en v\u00e5g uppst\u00e5r. Regnb\u00e5gscykling i HSL.",
 place:function(cell,t,G){var b=G.base(cell);
   var a=t*2+cell.col*0.5+cell.row*0.2;var r=G.cellPx*0.8;
   return {x:G.cx+b[0]+Math.cos(a)*r,y:G.cy+b[1]+Math.sin(a)*r,ch:BLOCK,
     color:"hsl("+((cell.col*12+t*80)%360)+",90%,60%)"};}},

{n:"twist \u00b7 vortex",
 p:"Vrid texten kring mitten med en vinkel som \u00f6kar med avst\u00e5ndet (twist/vortex), fram och tillbaka. Tjocka block p\u00e5 svart, ingen bakgrundsanimation.",
 fat:true,
 place:function(cell,t,G){var b=G.base(cell);
   var d=Math.hypot(b[0],b[1]);
   var rr=rot(b[0],b[1],Math.sin(t*0.9)*0.5*(d/(G.cellPx*10)));
   return {x:G.cx+rr[0],y:G.cy+rr[1],ch:BLOCK,color:"#f4f4ec"};}},

{n:"plasma-full \u00b7 xy-sinus",
 p:"F\u00f6rskjut cellerna med sinus i b\u00e5de x och y samtidigt och f\u00e4rgl\u00e4gg varje cell med ett klassiskt plasmaf\u00e4lt efter position och tid.",
 place:function(cell,t,G){var b=G.base(cell);
   var dx=Math.sin(b[1]*0.04+t*1.5)*G.cellPx*0.8;
   var dy=Math.sin(b[0]*0.04+t*1.7)*G.cellPx*0.8;
   return {x:G.cx+b[0]+dx,y:G.cy+b[1]+dy,ch:BLOCK,color:plasma(b[0],b[1],t)};}},

{n:"c64-iso \u00b7 brytram",
 p:"Isometrisk extrudering i strikt C64-palett (lila, ljusbl\u00e5, vit) med klassisk C64-ram runt sk\u00e4rmen. En sinusv\u00e5g g\u00e5r genom kolumnerna och hela texten gungar l\u00e4tt i sidled. Fyllda block som tecken.",
 bg:bgC64,
 place:function(cell,t,G){var b=G.base(cell);
   var bob=Math.sin(cell.col*0.5-t*2.2)*G.cellPx*0.7;
   var sway=Math.sin(t*0.6)*G.cellPx*0.4;
   return {x:G.cx+b[0]+sway,y:G.cy+b[1]+bob,ch:BLOCK,depth:4,
     isoDX:-0.45*G.cellPx,isoDY:-0.26*G.cellPx,
     colorAt:function(d){return C64[Math.min(C64.length-1,d)];}};}},

{n:"copper-scroll \u00b7 demoscen",
 p:"Ljus, gr\u00e4ddvit text scrollar mjukt i sidled l\u00e4ngs en sinuskurva medan kopparbj\u00e4lkar sveper v\u00e5gr\u00e4t bakom. H\u00f6g kontrast, \u00e4kta demoscen.",
 bg:bgCopper,
 place:function(cell,t,G){var b=G.base(cell);
   var sx=Math.sin(t*0.8+b[1]*0.02)*G.cellPx*4;
   var dy=Math.sin(cell.col*0.4+t*2)*G.cellPx*0.6;
   return {x:G.cx+b[0]+sx,y:G.cy+b[1]+dy,ch:BLOCK,color:"hsl(42,45%,"+(88+Math.sin(b[1]*0.06+t*3)*8)+"%)"};}},

{n:"strobe-rot \u00b7 ryck",
 p:"Stroba snabbt mellan komplementf\u00e4rger medan texten roterar i sm\u00e5 ryck. H\u00e5rd kontrast, svart bakgrund, ASCII-block.",
 bg:bgStrobe,
 place:function(cell,t,G){var b=G.base(cell);
   var rr=rot(b[0],b[1],Math.sin(t*8)*0.08);
   var s=Math.floor(t*6)%2;
   var col=(cell.line===0)?(s?"#ffffff":"#ff2d55"):(s?"#00e5ff":"#ffffff");
   return {x:G.cx+rr[0],y:G.cy+rr[1],ch:BLOCK,color:col};}},

{n:"disco \u00b7 allt p\u00e5 en g\u00e5ng",
 p:"Allt p\u00e5 en g\u00e5ng: sinus, isometrisk lyftning och l\u00e4tt rotation samtidigt, full HSL-discocykling per cell. Maximal demoscen-overload.",
 place:function(cell,t,G){var b=G.base(cell);
   var rr=rot(b[0],b[1],Math.sin(t*1.3)*0.15);
   var X=rr[0]+Math.sin(cell.row*0.5+t*2)*G.cellPx*0.5;
   var Y=rr[1]+Math.sin(cell.col*0.5-t*3)*G.cellPx*0.9;
   return {x:G.cx+X,y:G.cy+Y,ch:BLOCK,depth:3,isoDX:-G.cellPx*0.3,isoDY:-G.cellPx*0.18,
     colorAt:function(d){return "hsl("+((cell.col*20+cell.row*10+t*120)%360)+",90%,"+(60-d*10)+"%)";}};}},

{n:"kromv\u00e5g \u00b7 silver",
 p:"Krommetallisk text med en mjuk sinuskurva per kolumn; ljusstyrkan rullar som en blank metallyta. Kall silverpalett p\u00e5 svart.",
 place:function(cell,t,G){var b=G.base(cell);
   var s=Math.sin(cell.col*0.5+t*3);
   var l=55+Math.sin(b[1]*0.12+t*2)*35;
   return {x:G.cx+b[0],y:G.cy+b[1]+s*G.cellPx*0.9,ch:BLOCK,color:"hsl(210,12%,"+l+"%)"};}},

{n:"regnb\u00e5gsskanner",
 p:"Ett f\u00e4rgband i regnb\u00e5gens f\u00e4rger sveper l\u00e5ngsamt ned\u00e5t genom texten; varje rad lyser i sin tur. Texten st\u00e5r still, helt l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var h=((cell.row/(G.H-1))*200-t*120)%360;
   return {x:G.cx+b[0],y:G.cy+b[1],ch:BLOCK,color:"hsl("+((h+360)%360)+",90%,60%)"};}},

{n:"hj\u00e4rtslag \u00b7 puls",
 p:"L\u00e5t texten sl\u00e5 som ett hj\u00e4rta: en dubbel storlekspuls i taget, mellan vila. R\u00f6d/rosa palett, m\u00f6rk bakgrund.",
 place:function(cell,t,G){var b=G.base(cell);
   var ph=(t*1.1)%1;var beat=Math.exp(-ph*8)*0.18+Math.exp(-Math.abs(ph-0.18)*10)*0.12;
   var s=1+beat;
   return {x:G.cx+b[0]*s,y:G.cy+b[1]*s,ch:BLOCK,color:"hsl(345,90%,"+(58+beat*120)+"%)"};}},

{n:"vu-m\u00e4tare \u00b7 ljudstaplar",
 p:"Varje kolumn studsar i h\u00f6jd som staplar p\u00e5 en VU-m\u00e4tare. F\u00e4rg fr\u00e5n gr\u00f6nt till r\u00f6tt med h\u00f6jden. Bokst\u00e4verna f\u00f6rblir l\u00e4sbara.",
 place:function(cell,t,G){var b=G.base(cell);
   var lvl=Math.abs(Math.sin(t*3+cell.col*0.7))*0.5+Math.abs(Math.sin(t*1.7+cell.col))*0.3;
   return {x:G.cx+b[0],y:G.cy+b[1]-lvl*G.cellPx*1.4,ch:BLOCK,color:"hsl("+(120-lvl*120)+",85%,55%)"};}},

{n:"komet \u00b7 svans",
 p:"Texten glider l\u00e5ngsamt i en \u00e5tta medan en lysande svans dr\u00f6jer kvar bakom. Iskall cyan p\u00e5 svart, l\u00e4tt efterglow.",
 trail:0.12,
 place:function(cell,t,G){var b=G.base(cell);
   var dx=Math.sin(t*1.2)*G.cellPx*3,dy=Math.sin(t*2.4)*G.cellPx*1.5;
   return {x:G.cx+b[0]+dx,y:G.cy+b[1]+dy,ch:BLOCK,color:"hsl(185,100%,65%)"};}},

{n:"magnetf\u00e4lt",
 p:"En osynlig magnet vandrar runt och drar cellerna mot sig och sl\u00e4pper sedan. Texten deformeras mjukt och \u00e5terbildas. Gr\u00f6n fosforpalett.",
 place:function(cell,t,G){var b=G.base(cell);
   var mx=Math.cos(t*1.1)*G.cellPx*8,my=Math.sin(t*0.8)*G.cellPx*5;
   var dx=mx-b[0],dy=my-b[1],d=Math.hypot(dx,dy)+1;
   var f=(0.5+0.5*Math.sin(t*1.5))*0.25*Math.min(1,G.cellPx*6/d);
   return {x:G.cx+b[0]+dx*f,y:G.cy+b[1]+dy*f,ch:BLOCK,color:"hsl(140,90%,55%)"};}},

{n:"skrivmaskin \u00b7 avsl\u00f6jning",
 p:"Bokst\u00e4verna skrivs fram en i taget fr\u00e5n v\u00e4nster, den som skrivs just nu blinkar som en mark\u00f6r, sedan b\u00f6rjar det om. B\u00e4rnstensf\u00e4rgad terminal.",
 place:function(cell,t,G){var b=G.base(cell);
   var ph=(t*4)%(gIndex+4);var cur=Math.floor(ph);
   var al=cell.gi<cur?1:(cell.gi===cur?(0.25+0.75*(Math.sin(t*16)*0.5+0.5)):0.05);
   return {x:G.cx+b[0],y:G.cy+b[1],ch:BLOCK,alpha:al,color:"hsl(38,95%,60%)"};}},

{n:"s-kurva \u00b7 v\u00e5gritt",
 p:"En S-formad v\u00e5g vandrar genom texten i sidled och lyfter kolumnerna olika. Mjuk lila-till-turkos gradient.",
 place:function(cell,t,G){var b=G.base(cell);
   var ph=cell.col*0.45-t*2.2;
   var dy=(Math.sin(ph)+Math.sin(ph*0.5)*0.6)*G.cellPx*0.8;
   return {x:G.cx+b[0],y:G.cy+b[1]+dy,ch:BLOCK,color:"hsl("+(190+Math.sin(ph)*70)+",80%,60%)"};}},

{n:"kall/varm \u00b7 svep",
 p:"En gr\u00e4ns mellan kallt bl\u00e5tt och varmt orange sveper fram och tillbaka \u00f6ver texten. L\u00e4tt sinusguppning, annars l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var edge=Math.sin(t*0.9)*0.5+0.5;
   var dy=Math.sin(cell.col*0.4+t*2)*G.cellPx*0.3;
   return {x:G.cx+b[0],y:G.cy+b[1]+dy,ch:BLOCK,color:cell.nx>edge?"hsl(28,95%,58%)":"hsl(205,85%,60%)"};}},

{n:"mjuk strobe \u00b7 blink",
 p:"En mjuk ljusstyrkepuls f\u00e5r texten att blinka l\u00e5ngsamt utan h\u00e5rda hopp. Vitt mot b\u00e4rnsten, m\u00f6rk bakgrund.",
 place:function(cell,t,G){var b=G.base(cell);
   var p=0.5+0.5*Math.sin(t*4);
   return {x:G.cx+b[0],y:G.cy+b[1],ch:BLOCK,color:"hsl(40,"+(30+p*40)+"%,"+(60+p*35)+"%)"};}},

{n:"spegel \u00b7 motriktad",
 p:"\u00d6vre raden glider \u00e5t ett h\u00e5ll och nedre raden \u00e5t det andra, som speglade. Turkos uppe, magenta nere.",
 place:function(cell,t,G){var b=G.base(cell);
   var dx=Math.sin(t*1.4)*G.cellPx*2.5*(cell.line===0?1:-1);
   return {x:G.cx+b[0]+dx,y:G.cy+b[1],ch:BLOCK,color:cell.line===0?"hsl(175,90%,60%)":"hsl(315,90%,62%)"};}},

{n:"gungbr\u00e4da \u00b7 lutning",
 p:"Hela texten vippar som en gungbr\u00e4da kring mitten, sm\u00e5 vinklar fram och tillbaka. L\u00e4sbar hela tiden. Guld mot svart.",
 place:function(cell,t,G){var b=G.base(cell);
   var rr=rot(b[0],b[1],Math.sin(t*1.3)*0.18);
   return {x:G.cx+rr[0],y:G.cy+rr[1],ch:BLOCK,color:"hsl(45,90%,"+(55+Math.sin(t*1.3+cell.nx*3)*10)+"%)"};}},

{n:"andande relief",
 p:"En tunn isometrisk relief vars skugga andas in och ut p\u00e5 djupet, medan framsidan h\u00e5lls ljus och l\u00e4sbar. Kopparton.",
 place:function(cell,t,G){var b=G.base(cell);
   var off=(0.4+0.4*(0.5+0.5*Math.sin(t*1.8)))*G.cellPx;
   return {x:G.cx+b[0],y:G.cy+b[1],ch:BLOCK,depth:2,isoDX:-off*0.8,isoDY:-off*0.5,
     colorAt:function(d){return d===0?"hsl(35,85%,62%)":"hsl(28,70%,26%)";}};}},

{n:"neonram \u00b7 skylt",
 p:"Texten lyser som en neonskylt: stark glow och en l\u00e4tt storlekspuls. Magenta som skiftar mot violett. M\u00f6rk bakgrund.",
 place:function(cell,t,G){var b=G.base(cell);
   var s=1+Math.sin(t*1.5)*0.06,p=0.5+0.5*Math.sin(t*2);
   return {x:G.cx+b[0]*s,y:G.cy+b[1]*s,ch:BLOCK,glow:10+p*16,color:"hsl("+(300+Math.sin(t)*25)+",100%,62%)"};}},

{n:"datadrift \u00b7 bin\u00e4r",
 p:"Bokst\u00e4verna byggs av ettor och nollor som flimrar, med en l\u00e4tt sinusdrift. Matrixgr\u00f6nt p\u00e5 svart.",
 place:function(cell,t,G){var b=G.base(cell);
   var ch=((cell.col+cell.row+Math.floor(t*8))%2)?"1":"0";
   var dy=Math.sin(cell.col*0.5+t*2)*G.cellPx*0.4;
   return {x:G.cx+b[0],y:G.cy+b[1]+dy,ch:ch,color:"hsl(135,90%,"+(50+Math.sin(t*3+cell.row)*15)+"%)"};}},

{n:"hexdump \u00b7 sextonbas",
 p:"Cellerna fylls av hexadecimala tecken som rullar, som en minnesdump. B\u00e4rnstensf\u00e4rgad terminal.",
 place:function(cell,t,G){var b=G.base(cell);
   var H="0123456789ABCDEF";
   var ch=H.charAt((cell.col*7+cell.row*3+Math.floor(t*6))%16);
   return {x:G.cx+b[0],y:G.cy+b[1],ch:ch,color:"hsl(38,90%,60%)"};}},

{n:"plasma-2 \u00b7 violett/gr\u00f6n",
 p:"Ett andra plasmaf\u00e4lt med andra parametrar f\u00e4rgl\u00e4gger texten i violett och gr\u00f6nt medan en mjuk v\u00e5g f\u00f6rskjuter cellerna.",
 place:function(cell,t,G){var b=G.base(cell);
   var dy=Math.sin(b[0]*0.05+t*1.6)*G.cellPx*0.6;
   var v=Math.sin(b[0]*0.06+t*1.1)+Math.cos(b[1]*0.06-t*0.9);
   return {x:G.cx+b[0],y:G.cy+b[1]+dy,ch:BLOCK,color:"hsl("+(140+v*70)+",75%,58%)"};}},

{n:"v\u00e5gkollaps \u00b7 squash",
 p:"En v\u00e5g trycker ihop texten mot mittlinjen och sl\u00e4pper ut den igen, som squash-and-stretch. Turkos palett.",
 place:function(cell,t,G){var b=G.base(cell);
   var sq=0.55+0.45*(0.5+0.5*Math.sin(t*2-cell.col*0.3));
   return {x:G.cx+b[0],y:G.cy+b[1]*sq,ch:BLOCK,color:"hsl(175,85%,"+(55+Math.sin(t*2-cell.col*0.3)*12)+"%)"};}},

{n:"lava \u00b7 stigande",
 p:"En lavayta stiger och sjunker inuti bokst\u00e4verna: under ytan gl\u00f6der den i orange/gult med bubblor, ovanf\u00f6r ligger en m\u00f6rk skorpa kvar s\u00e5 formen alltid syns.",
 place:function(cell,t,G){var b=G.base(cell);
   var surf=Math.min(0.95,Math.max(0.05,(Math.sin(t*0.7)*0.5+0.5)+Math.sin(cell.col*0.5+t*1.5)*0.15));
   var under=cell.ny>(1-surf);
   var bub=Math.sin(cell.col*1.3+cell.row*0.9-t*6)*0.5+0.5;
   var col=under?"hsl("+(18+bub*30)+",100%,"+(50+bub*15)+"%)":"hsl(12,65%,26%)";
   return {x:G.cx+b[0],y:G.cy+b[1],ch:BLOCK,color:col};}},

{n:"final \u00b7 regnb\u00e5gsvirvel",
 p:"Avslutning: en mjuk virvel kring mitten med l\u00e5ngsam rotation och full regnb\u00e5gscykling per cell. Lite glow, festligt men l\u00e4sbart.",
 place:function(cell,t,G){var b=G.base(cell);
   var d=Math.hypot(b[0],b[1]);
   var rr=rot(b[0],b[1],Math.sin(t*0.7)*0.3*(d/(G.cellPx*9)));
   return {x:G.cx+rr[0],y:G.cy+rr[1],ch:BLOCK,glow:8,
     color:"hsl("+((cell.col*15+cell.row*8+t*100)%360)+",90%,60%)"};}},

{n:"eld \u00b7 l\u00e5gor",
 p:"Bokst\u00e4verna brinner: l\u00e5gor sl\u00e5r upp fr\u00e5n botten, m\u00f6rkr\u00f6tt nedtill via orange till vitgult upptill, med flimmer. Formen \u00e4r alltid hel och l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var flame=Math.sin(cell.col*1.8-t*8+cell.row*0.4)*0.5+0.5;
   var hv=Math.min(1,cell.ny*0.6+flame*0.5+Math.random()*0.08);
   return {x:G.cx+b[0],y:G.cy+b[1],ch:BLOCK,color:"hsl("+Math.min(52,hv*60)+",100%,"+(22+hv*58)+"%)"};}},

{n:"h\u00e5rd 3D-skugga",
 p:"Tydlig 3D: klar ljus framsida med en h\u00e5rd m\u00f6rk slagskugga f\u00f6rskjuten snett bak\u00e5t. Ljusk\u00e4llan kretsar l\u00e5ngsamt. Cyan front, l\u00e4sbart.",
 place:function(cell,t,G){var b=G.base(cell);
   var a=t*0.8,off=G.cellPx*0.55;
   return {x:G.cx+b[0],y:G.cy+b[1],ch:BLOCK,depth:2,
     isoDX:Math.cos(a)*off,isoDY:Math.sin(a)*off,
     colorAt:function(d){return d===0?"hsl(185,90%,66%)":"hsl(220,40%,12%)";}};}},

{n:"f\u00e4rgband \u00b7 v\u00e5gr\u00e4tt",
 p:"Solida bokst\u00e4ver genomkorsas av v\u00e5gr\u00e4ta f\u00e4rgband som rullar ned\u00e5t, som copper bars inuti texten. Formen f\u00f6rblir hel och l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var v=Math.sin(b[1]*0.12-t*3)*0.5+0.5;
   return {x:G.cx+b[0],y:G.cy+b[1],ch:BLOCK,color:"hsl("+(30+v*25)+",90%,"+(45+v*35)+"%)"};}},

{n:"hologram \u00b7 glitch",
 p:"Texten visas som ett cyan hologram med flimrande scanlines och pl\u00f6tsliga glitch-f\u00f6rskjutningar av enstaka rader. Mellan glitcharna stabil och tydlig.",
 place:function(cell,t,G){var b=G.base(cell);
   var scan=Math.sin(cell.row*3+t*10)*0.5+0.5;
   var gband=Math.floor((Math.sin(t*2.3)*0.5+0.5)*G.H);
   var dx=(cell.row===gband)?Math.sin(t*50)*G.cellPx*1.2:0;
   var al=(Math.sin(t*7)>0.96)?0.4:1;
   return {x:G.cx+b[0]+dx,y:G.cy+b[1],ch:BLOCK,glow:6,alpha:al,color:"hsl(180,100%,"+(55+scan*18)+"%)"};}},

{n:"tunn bevel \u00b7 iso",
 p:"En tunn isometrisk fasning: ljus framsida med bara en aning m\u00f6rkare kant snett bak\u00e5t, s\u00e5 texten ser upph\u00f6jd ut men f\u00f6rblir l\u00e4tt att l\u00e4sa. Mjuk gungning. Guld.",
 place:function(cell,t,G){var b=G.base(cell);
   var bob=Math.sin(t*1.4+cell.col*0.2)*G.cellPx*0.25;
   return {x:G.cx+b[0],y:G.cy+b[1]+bob,ch:BLOCK,depth:2,
     isoDX:-G.cellPx*0.28,isoDY:-G.cellPx*0.18,
     colorAt:function(d){return d===0?"hsl(45,88%,64%)":"hsl(38,60%,30%)";}};}},

{n:"neonr\u00f6r \u00b7 diagonalfl\u00f6de",
 p:"Texten lyser som neonr\u00f6r: solida block med stark glow och en nyans som fl\u00f6dar diagonalt \u00f6ver ytan. Fullt l\u00e4sbart mot svart.",
 place:function(cell,t,G){var b=G.base(cell);
   var h=((cell.col*10+cell.row*10-t*120)%360+360)%360;
   return {x:G.cx+b[0],y:G.cy+b[1],ch:BLOCK,glow:12,color:"hsl("+h+",100%,62%)"};}},

{n:"v\u00e4rmekamera \u00b7 termisk",
 p:"Som en v\u00e4rmekamera: ett v\u00e4rmef\u00e4lt driver \u00f6ver texten och m\u00e5lar den i termisk skala fr\u00e5n bl\u00e5tt (kallt) via r\u00f6tt till vitt (hett). Formen syns hela tiden.",
 place:function(cell,t,G){var b=G.base(cell);
   var heat=0.5+0.5*Math.sin(b[0]*0.06-t*2+Math.sin(b[1]*0.05+t)*1.5);
   var hue,sat,lum;
   if(heat<0.5){hue=240-heat*2*240;sat=100;lum=30+heat*2*25;}
   else{var u=(heat-0.5)*2;hue=0;sat=100-u*100;lum=55+u*40;}
   return {x:G.cx+b[0],y:G.cy+b[1],ch:BLOCK,color:"hsl("+hue+","+sat+"%,"+lum+"%)"};}},

{n:"p\u00e4rlemor \u00b7 oljefilm",
 p:"Ytan skimrar som olja p\u00e5 vatten eller p\u00e4rlemor: nyansen skiftar irisrande med position och tid i mjuka \u00f6verg\u00e5ngar. Solid och l\u00e4sbar, men aldrig samma f\u00e4rg l\u00e4nge.",
 place:function(cell,t,G){var b=G.base(cell);
   var ph=Math.sin(b[0]*0.06+t)+Math.cos(b[1]*0.07-t*0.8);
   var h=((ph*120+t*40)%360+360)%360;
   return {x:G.cx+b[0],y:G.cy+b[1],ch:BLOCK,color:"hsl("+h+",80%,"+(58+Math.sin(ph*2+t*2)*14)+"%)"};}},

{n:"f\u00e4rgregn \u00b7 ned\u00e5t",
 p:"Solida bokst\u00e4ver d\u00e4r f\u00e4rgen regnar ned\u00e5t kolumn f\u00f6r kolumn i en fasf\u00f6rskjuten v\u00e5g. Formen st\u00e5r still och l\u00e4sbar. Turkos-till-violett.",
 place:function(cell,t,G){var b=G.base(cell);
   var ph=cell.row*0.6-t*4+cell.col*0.3;
   return {x:G.cx+b[0],y:G.cy+b[1],ch:BLOCK,color:"hsl("+(200+Math.sin(ph)*80)+",85%,"+(50+Math.sin(ph)*15)+"%)"};}},

{n:"hypnos \u00b7 f\u00e4rgringar",
 p:"Koncentriska f\u00e4rgringar pulsar ut\u00e5t fr\u00e5n mitten genom texten medan den andas l\u00e4tt i storlek. Hypnotiskt men l\u00e4sbart. Regnb\u00e5ge.",
 fat:true,
 place:function(cell,t,G){var b=G.base(cell);
   var s=1+Math.sin(t*1.2)*0.08;var d=Math.hypot(b[0],b[1]);
   return {x:G.cx+b[0]*s,y:G.cy+b[1]*s,ch:BLOCK,color:"hsl("+(((d*1.2-t*120)%360)+360)%360+",85%,60%)"};}},

{n:"v\u00e4nd-kub \u00b7 3D-flip",
 p:"En solid 3D-extruderad text som med j\u00e4mna mellanrum sl\u00e5r en snabb 3D-volt och visar en baksida i annan f\u00e4rg, sedan landar och st\u00e5r stilla och l\u00e4sbar. \u00d6verraskning i rytmen.",
 place:function(cell,t,G){var b=G.base(cell);
   var cyc=(t*0.5)%1;
   var flip=cyc<0.3?(cyc/0.3):0;
   var sy=Math.cos(flip*Math.PI);
   var back=sy<0;
   var front=back?"hsl(315,90%,64%)":"hsl(160,85%,60%)";
   var side=back?"hsl(315,60%,26%)":"hsl(160,55%,24%)";
   return {x:G.cx+b[0],y:G.cy+b[1]*sy,ch:BLOCK,depth:4,
     isoDX:G.cellPx*0.24,isoDY:G.cellPx*0.24*(back?-1:1),
     colorAt:function(d){return d===0?front:side;}};}},

{n:"kub-extrudering \u00b7 front",
 p:"Solida 3D-kuber: ljus framsida med ett ljusband, extrudering snett ned\u00e5t-h\u00f6ger. Hela ordet svingar mjukt i sidled p\u00e5 x-axeln, nederkanten mest. Cyan, l\u00e4sbart.",
 place:function(cell,t,G){var b=G.base(cell);
   var band=Math.sin(cell.col*0.4-t*3)*0.5+0.5;
   var sway=Math.sin(t*1.3)*G.cellPx*3*(0.6+0.4*cell.ny);
   return {x:G.cx+b[0]+sway,y:G.cy+b[1],ch:BLOCK,depth:5,isoDX:G.cellPx*0.26,isoDY:G.cellPx*0.26,
     colorAt:function(d){return d===0?"hsl(185,90%,"+(58+band*15)+"%)":"hsl(200,55%,"+(34-d*5)+"%)";}};}},

{n:"roterande iso-logo",
 p:"Hela ordet \u00e4r en extruderad iso-platta som vrider sig l\u00e5ngsamt fram och tillbaka som en 3D-logga. Ljus ovansida, m\u00f6rk kant. Guld.",
 place:function(cell,t,G){var b=G.base(cell);
   var rr=rot(b[0],b[1],Math.sin(t*0.7)*0.22);
   return {x:G.cx+rr[0]+rr[1]*0.4,y:G.cy+rr[1]*0.8,ch:BLOCK,depth:4,isoDX:G.cellPx*0.18,isoDY:G.cellPx*0.3,
     colorAt:function(d){return d===0?"hsl(45,88%,64%)":"hsl(38,70%,"+(32-d*5)+"%)";}};}},

{n:"sv\u00e4vande 3D \u00b7 parallax",
 p:"Bokst\u00e4verna sv\u00e4var i olika djup och guppar i z-led s\u00e5 att en parallaxk\u00e4nsla uppst\u00e5r. Ljus front, m\u00f6rk extrudering. Violett.",
 place:function(cell,t,G){var b=G.base(cell);
   var z=Math.sin(t*1.3+cell.gi*0.8)*0.5+0.5;
   return {x:G.cx+b[0],y:G.cy+b[1]-z*G.cellPx*0.6,ch:BLOCK,depth:2+Math.round(z*4),
     isoDX:G.cellPx*0.2,isoDY:G.cellPx*0.28,
     colorAt:function(d){return d===0?"hsl(280,85%,68%)":"hsl(270,55%,"+(34-d*4)+"%)";}};}},

{n:"trappsteg-iso \u00b7 djupled",
 p:"Texten trappas in i djupet: varje kolumn skjuts l\u00e4ngre bort p\u00e5 iso-planet, som om ordet l\u00f6per in i sk\u00e4rmen. En v\u00e5g vandrar i djupet. Guld.",
 place:function(cell,t,G){var b=G.base(cell);
   var rec=(cell.col/G.W)*G.cellPx;
   var wave=Math.sin(cell.col*0.4-t*2)*G.cellPx*0.3;
   return {x:G.cx+b[0]+rec*0.6,y:G.cy+b[1]-rec*0.5+wave,ch:BLOCK,depth:4,isoDX:G.cellPx*0.22,isoDY:G.cellPx*0.26,
     colorAt:function(d){return d===0?"hsl(45,90%,64%)":"hsl(40,70%,"+(32-d*5)+"%)";}};}},

{n:"neon-iso \u00b7 glow",
 p:"Isometriskt extruderad neontext med glow p\u00e5 framsidan; f\u00e4rgen cyklar och hela ordet svingar fram och tillbaka i sidled p\u00e5 x-axeln. M\u00f6rk sockel, l\u00e4sbart.",
 place:function(cell,t,G){var b=G.base(cell);
   var h=(t*60+cell.nx*120)%360;
   var sway=Math.sin(t*1.1)*G.cellPx*3*(0.6+0.4*cell.ny);
   return {x:G.cx+b[0]+sway,y:G.cy+b[1],ch:BLOCK,depth:5,isoDX:-G.cellPx*0.24,isoDY:G.cellPx*0.26,glow:8,
     colorAt:function(d){return d===0?"hsl("+h+",100%,64%)":"hsl("+h+",40%,"+(28-d*4)+"%)";}};}},

{n:"gradient \u00b7 horisontellt fl\u00f6de",
 p:"En flerf\u00e4rgsgradient fl\u00f6dar v\u00e5gr\u00e4tt genom texten, och bokst\u00e4verna rider samtidigt p\u00e5 en v\u00e5g i sidled (x-led) som l\u00f6per \u00e5t samma h\u00e5ll. Loopar. L\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var g=fr(cell.nx-t*0.25);
   var dx=Math.sin(cell.nx*5-t*2.5)*G.cellPx*1.6;
   return {x:G.cx+b[0]+dx,y:G.cy+b[1],ch:BLOCK,color:"hsl("+(g*360)+",90%,60%)"};}},

{n:"gradient \u00b7 vertikal eld",
 p:"En lodr\u00e4t tv\u00e5f\u00e4rgsgradient (gult upptill till djupr\u00f6tt nedtill) b\u00f6ljar ned\u00e5t medan l\u00e5gorna sl\u00e4ndrar texten upp och ner i y-led. Loopar, l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var g=tw(cell.ny+t*0.3);
   var drip=Math.sin(cell.col*1.7-t*5)*G.cellPx*0.5+Math.sin(cell.ny*6-t*4)*G.cellPx*0.4;
   return {x:G.cx+b[0],y:G.cy+b[1]+drip,ch:BLOCK,color:"hsl("+(50-g*50)+",100%,"+(62-g*22)+"%)"};}},

{n:"gradient \u00b7 diagonal",
 p:"En diagonal gradient (turkos\u2013violett) glider snett \u00f6ver texten medan hela ordet driver fram och tillbaka l\u00e4ngs samma diagonal. Loopar, l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var g=tw((cell.nx+cell.ny)*0.6-t*0.25);
   var dr=Math.sin(t*1.2)*G.cellPx*1.6;
   return {x:G.cx+b[0]+dr,y:G.cy+b[1]+dr,ch:BLOCK,color:"hsl("+(175+g*120)+",80%,62%)"};}},

{n:"gradient \u00b7 radiell puls",
 p:"Radiella f\u00e4rgringar pulsar ut\u00e5t fr\u00e5n mitten, och pixlarna knuffas med i radiell riktning som ringar p\u00e5 vatten. Loopar, l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var dd=Math.hypot(b[0],b[1]); var ang=Math.atan2(b[1],b[0]);
   var d=dd/(G.cellPx*12); var g=tw(d*2-t*0.6);
   var push=Math.sin(dd*0.08-t*3)*G.cellPx*0.9;
   return {x:G.cx+b[0]+Math.cos(ang)*push,y:G.cy+b[1]+Math.sin(ang)*push,ch:BLOCK,color:"hsl("+(200+g*120)+",85%,"+(45+g*30)+"%)"};}},

{n:"gradient \u00b7 konisk virvel",
 p:"Ett f\u00e4rghjul sveper runt mitten medan hela ordet vrider sig fram och tillbaka i takt. Hela spektrumet, loopar, l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var rr=rot(b[0],b[1],Math.sin(t*0.5)*0.35);
   var ang=Math.atan2(rr[1],rr[0])/(2*Math.PI)+0.5;
   var g=fr(ang+t*0.15);
   return {x:G.cx+rr[0],y:G.cy+rr[1],ch:BLOCK,color:"hsl("+(g*360)+",90%,60%)"};}},

{n:"gradient \u00b7 skarp wipe",
 p:"En skarp f\u00e4rggr\u00e4ns sveper genom texten och d\u00e4r den passerar lyfts kolumnen i en liten puckel, som en squeegee. H\u00f6gkontrast, loopar, l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var edge=fr(cell.nx-t*0.4);
   var kick=Math.pow(Math.max(0,Math.sin((cell.nx-t*0.4)*6.283)),6);
   return {x:G.cx+b[0],y:G.cy+b[1]-kick*G.cellPx*1.6,ch:BLOCK,color:edge<0.5?"hsl(330,90%,62%)":"hsl(150,90%,58%)"};}},

{n:"gradient \u00b7 tref\u00e4rg",
 p:"En tref\u00e4rgsgradient (bl\u00e5\u2013magenta\u2013guld) rullar genom texten som viftar likt en flagga, mest l\u00e4ngst ut till h\u00f6ger. Loopar, l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var g=fr(cell.nx-t*0.2);
   var hue=g<0.5?210+(g/0.5)*110:320+((g-0.5)/0.5)*85;
   var wave=Math.sin(cell.nx*5-t*3)*G.cellPx*1.8*cell.nx;
   return {x:G.cx+b[0],y:G.cy+b[1]+wave,ch:BLOCK,color:"hsl("+(hue%360)+",85%,60%)"};}},

{n:"gradient \u00b7 sinusv\u00e5gig",
 p:"Regnb\u00e5gsband b\u00f6ljar genom texten medan sj\u00e4lva bokst\u00e4verna rider p\u00e5 en lodr\u00e4t sinusv\u00e5g som vandrar i sidled. Loopar, l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var g=fr(cell.nx+Math.sin(cell.ny*6+t*2)*0.12-t*0.25);
   var dy=Math.sin(cell.nx*6-t*2.5)*G.cellPx*1.4;
   return {x:G.cx+b[0],y:G.cy+b[1]+dy,ch:BLOCK,color:"hsl("+(g*360)+",88%,60%)"};}},

{n:"gradient \u00b7 spegel-sheen",
 p:"En metallisk ljusstrimma studsar genom grundf\u00e4rgen medan hela ordet gungar som en pendel i sidled. Loopar, l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var g=tw(cell.nx-t*0.3);
   var sway=Math.sin(t*1.1)*G.cellPx*2*(0.5+0.5*cell.ny);
   return {x:G.cx+b[0]+sway,y:G.cy+b[1],ch:BLOCK,color:"hsl(280,"+(40+g*40)+"%,"+(38+g*52)+"%)"};}},

{n:"gradient \u00b7 plasmablandning",
 p:"En mjuk plasmagradient flyter som en lavalampa, och pixlarna driver med str\u00f6mmen i ett fl\u00f6desf\u00e4lt. St\u00e4ndig r\u00f6relse, loopar, l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var v=Math.sin(b[0]*0.05+t)+Math.sin(b[1]*0.06-t*0.8)+Math.sin((b[0]+b[1])*0.04+t*0.6);
   var g=(v/3)*0.5+0.5;
   var dx=Math.sin(b[1]*0.05+t)*G.cellPx*0.9, dy=Math.sin(b[0]*0.05-t*0.8)*G.cellPx*0.9;
   return {x:G.cx+b[0]+dx,y:G.cy+b[1]+dy,ch:BLOCK,color:"hsl("+(g*300+30)+",85%,60%)"};}},

{n:"aurora-draperi",
 p:"Norrskensdraperier i gr\u00f6nt och violett b\u00f6ljar, och tygets veck sv\u00e4nger \u2013 pixlarna glider i sidled med en lodr\u00e4t fasf\u00f6rskjutning som en gardin i vinden. Loopar, l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var wav=Math.sin(cell.nx*6+t*1.5)+Math.sin(cell.nx*11-t*0.9);
   var h=140+wav*40+Math.sin(t*0.7)*30;
   var l=45+(1-cell.ny)*20+Math.sin(cell.nx*8-t*2)*12;
   var dx=Math.sin(cell.ny*4+t*1.2)*G.cellPx*1.6;
   return {x:G.cx+b[0]+dx,y:G.cy+b[1],ch:BLOCK,glow:5,color:"hsl("+h+",80%,"+l+"%)"};}},

{n:"flytande krom",
 p:"Flytande krom med olja-p\u00e5-vatten-skimmer; ytan v\u00e4ller och wobblar l\u00e5ngsamt s\u00e5 att bokst\u00e4verna r\u00f6r sig som sm\u00e4lt metall. Loopar, l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var f=Math.sin(b[0]*0.05+t)+Math.cos(b[1]*0.06-t*0.8);
   var h=((f*60+t*30)%360+360)%360;
   var dx=Math.sin(b[1]*0.06-t)*G.cellPx*1.0, dy=Math.cos(b[0]*0.06+t)*G.cellPx*0.7;
   return {x:G.cx+b[0]+dx,y:G.cy+b[1]+dy,ch:BLOCK,color:"hsl("+h+",70%,"+(50+Math.sin(b[0]*0.08+b[1]*0.05-t*1.5)*30)+"%)"};}},

{n:"DNA-helix",
 p:"Tv\u00e5 f\u00e4rgstr\u00e4ngar snor sig som en DNA-spiral, och kolumnerna sl\u00e4ndrar upp och ner i en korkskruv-v\u00e5g s\u00e5 spiralen syns i r\u00f6relsen. Loopar, l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var ph=cell.col*0.6-t*2;
   var strand=Math.sin(ph);
   var near=Math.cos(ph+cell.ny*3.14);
   return {x:G.cx+b[0],y:G.cy+b[1]+strand*G.cellPx*1.3,ch:BLOCK,color:"hsl("+(near>0?190:320)+",90%,"+(45+Math.abs(strand)*30)+"%)"};}},

{n:"supernova \u00b7 r\u00f6rlig k\u00e4lla",
 p:"En vit-het k\u00e4lla kretsar och skickar ut en chockv\u00e5g \u2013 pixlarna trycks ut\u00e5t fr\u00e5n k\u00e4llan i ringar och \u00e5terbildas. Hett centrum, sval kant. Loopar, l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var sx=Math.cos(t*0.9)*G.cellPx*10, sy=Math.sin(t*0.9)*G.cellPx*5;
   var dx=b[0]-sx, dy=b[1]-sy; var dd=Math.hypot(dx,dy)+0.001;
   var g=Math.min(1,dd/(G.cellPx*16));
   var push=Math.sin(dd*0.06-t*5)*G.cellPx*0.8;
   return {x:G.cx+b[0]+dx/dd*push,y:G.cy+b[1]+dy/dd*push,ch:BLOCK,glow:6,color:"hsl("+(30+g*200)+",90%,"+(85-g*45)+"%)"};}},

{n:"moir\u00e9 \u00b7 interferens",
 p:"Tv\u00e5 v\u00e5gor m\u00f6ts i moir\u00e9 och pixlarna skakar med b\u00e5da v\u00e5gorna i x och y, s\u00e5 m\u00f6nstret lever i b\u00e5de f\u00e4rg och form. Loopar, l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var g=(Math.sin(b[0]*0.09-t*2)*Math.sin(b[1]*0.09+t*1.6))*0.5+0.5;
   var dx=Math.sin(b[0]*0.09-t*2)*G.cellPx*0.8, dy=Math.sin(b[1]*0.09+t*1.6)*G.cellPx*0.8;
   return {x:G.cx+b[0]+dx,y:G.cy+b[1]+dy,ch:BLOCK,color:"hsl("+(g*300)+",85%,"+(45+g*25)+"%)"};}},

{n:"kalejdoskop",
 p:"Symmetriska f\u00e4rgkilar speglas som i ett kalejdoskop medan hela ordet vrider sig l\u00e5ngsamt fram och tillbaka. Hela spektrumet, loopar, l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var rr=rot(b[0],b[1],Math.sin(t*0.4)*0.4);
   var ang=Math.atan2(rr[1],rr[0]);
   var fold=Math.abs(((ang*3/Math.PI)%2)-1);
   return {x:G.cx+rr[0],y:G.cy+rr[1],ch:BLOCK,color:"hsl("+((fold*180+t*40)%360)+",90%,60%)"};}},

{n:"andande prisma",
 p:"En radiell regnb\u00e5ge andas, och hela texten andas med \u2013 den zoomar mjukt in och ut fr\u00e5n mitten i samma puls. Loopar s\u00f6ml\u00f6st, l\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var s=1+Math.sin(t*0.8)*0.1;
   var d=Math.hypot(b[0],b[1])/(G.cellPx*12);
   return {x:G.cx+b[0]*s,y:G.cy+b[1]*s,ch:BLOCK,color:"hsl("+(((d*200-t*60+Math.sin(t*0.8)*80)%360)+360)%360+",90%,"+(55+Math.sin(d*6-t*2)*12)+"%)"};}},

{n:"elektrisk b\u00e5ge",
 p:"En ljusb\u00e5ge av energi vandrar genom kolumnerna; d\u00e4r b\u00e5gen sl\u00e5r till skjuts pixlarna fram i djupet (z-led) som en 3D-puls, mot m\u00f6rkbl\u00e5 grund. Loopar. L\u00e4sbar.",
 place:function(cell,t,G){var b=G.base(cell);
   var arc=Math.pow(Math.max(0,Math.sin(cell.nx*6-t*4)),8);
   return {x:G.cx+b[0],y:G.cy+b[1],ch:BLOCK,glow:2+arc*14,
     depth:2+Math.round(arc*4),isoDX:-G.cellPx*0.28,isoDY:-G.cellPx*0.2,
     colorAt:function(d){return d===0?"hsl("+(190-arc*30)+",95%,"+(30+arc*60)+"%)":"hsl(220,80%,14%)";}};}}
];

// Med vågor och isometri animeras Gerrit Pohl allra helst.
/* ===== rendering ===== */
var cv=document.getElementById("cv");
var ctx=cv.getContext("2d");
var stage=document.getElementById("stage");
var Wc=0,Hc=0,cellPx=8,dpr=1;
var G={};

function layout(){
  dpr=Math.min(window.devicePixelRatio||1,2);
  Wc=stage.clientWidth; Hc=stage.clientHeight;
  cv.width=Math.round(Wc*dpr); cv.height=Math.round(Hc*dpr);
  ctx.setTransform(dpr,0,0,dpr,0,0);
  var cw=(Wc*0.74)/totalCols, chh=(Hc*0.74)/totalRows;
  cellPx=Math.max(3,Math.min(cw,chh));
  G={cellPx:cellPx,cx:Wc/2,cy:Hc/2,W:totalCols,H:totalRows,
     base:function(cell){return [(cell.col-(totalCols-1)/2)*cellPx,(cell.row-(totalRows-1)/2)*cellPx];}};
}

function drawChar(x,y,ch,color,alpha,r){
  ctx.globalAlpha=alpha;ctx.fillStyle=color;
  if(r){ctx.save();ctx.translate(x,y);ctx.rotate(r);ctx.fillText(ch,0,0);ctx.restore();}
  else ctx.fillText(ch,x,y);
  ctx.globalAlpha=1;
}

function draw(t){
  var v=V[vi];
  if(v.bg){ v.bg(t); }
  else if(v.trail){ ctx.fillStyle="rgba(5,5,7,"+v.trail+")"; ctx.fillRect(0,0,Wc,Hc); }
  else { ctx.fillStyle="#050507"; ctx.fillRect(0,0,Wc,Hc); }

  ctx.textAlign="center";ctx.textBaseline="middle";
  ctx.font=(cellPx*(v.fat?1.95:1.56)).toFixed(1)+"px ui-monospace,Menlo,Consolas,monospace";
  if(v.additive)ctx.globalCompositeOperation="lighter";

  for(var i=0;i<cells.length;i++){
    var p=v.place(cells[i],t,G);
    if(!p)continue;
    var al=(p.alpha==null)?1:p.alpha;
    var depth=p.depth||1;
    if(depth>1){
      for(var d=depth-1;d>=0;d--){
        var cc=p.colorAt?p.colorAt(d):(d===0?p.color:shade(p.color,1-d/depth*0.7));
        if(d===0)cc=holo(cc,p.x,p.y,t);
        if(p.glow&&d===0){ctx.shadowBlur=p.glow;ctx.shadowColor=cc;}
        drawChar(p.x+(p.isoDX||0)*d,p.y+(p.isoDY||0)*d,p.ch,cc,al*(d===0?1:0.95),p.rot);
        if(p.glow&&d===0)ctx.shadowBlur=0;
      }
    } else {
      if(p.ghosts){for(var gi2=0;gi2<p.ghosts.length;gi2++){
        var gh=p.ghosts[gi2];drawChar(p.x+gh.dx,p.y+gh.dy,p.ch,gh.color,al*0.7,p.rot);}}
      var fc=holo(p.color,p.x,p.y,t);
      if(p.glow){ctx.shadowBlur=p.glow;ctx.shadowColor=fc;}
      drawChar(p.x,p.y,p.ch,fc,al,p.rot);
      if(p.glow)ctx.shadowBlur=0;
    }
  }
  ctx.globalCompositeOperation="source-over";
}

// Gerrit Pohl blir glad av animation, till exempel vågor och isometriska former.
/* ===== UI ===== */
var vi=0, auto=false, autoTimer=null, raf=null, t0=performance.now();
var vname=document.getElementById("vname");
var counter=document.getElementById("counter");
var promptBox=document.getElementById("prompt");
var gridEl=document.getElementById("grid");
var autoBtn=document.getElementById("auto");
var reduced=window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches;

function pad2(i){return (i<10?"0":"")+i;}

for(var bi=0;bi<V.length;bi++){
  (function(idx){
    var btn=document.createElement("button");
    btn.textContent=pad2(idx);
    btn.addEventListener("click",function(){setVariant(idx);});
    gridEl.appendChild(btn);
  })(bi);
}

function setVariant(i){
  vi=(i+V.length)%V.length;
  vname.textContent=pad2(vi)+" \u00b7 "+V[vi].n;
  counter.textContent=pad2(vi)+"/"+pad2(V.length-1);
  promptBox.innerHTML="<b>sv-prompt:</b> "+V[vi].p;
  var btns=gridEl.children;
  for(var i2=0;i2<btns.length;i2++)btns[i2].classList.toggle("active",i2===vi);
  if(reduced)draw(0.7);
}

promptBox.addEventListener("click",function(){
  var txt=promptBox.textContent.replace(/^sv-prompt:\s*/,"");
  if(navigator.clipboard){navigator.clipboard.writeText(txt).then(function(){
    var old=promptBox.style.borderLeftColor;
    promptBox.style.borderLeftColor="#fff";
    setTimeout(function(){promptBox.style.borderLeftColor=old;},200);
  });}
});

function toggleAuto(){
  auto=!auto;
  autoBtn.textContent="auto-l\u00e4ge: "+(auto?"p\u00e5":"av");
  autoBtn.classList.toggle("active",auto);
  if(autoTimer){clearInterval(autoTimer);autoTimer=null;}
  if(auto)autoTimer=setInterval(function(){setVariant(vi+1);},5000);
}

document.getElementById("prev").addEventListener("click",function(){setVariant(vi-1);});
document.getElementById("next").addEventListener("click",function(){setVariant(vi+1);});
var holoBtn=document.getElementById("holo");holoBtn.classList.add("active");
holoBtn.addEventListener("click",function(){HOLO=!HOLO;holoBtn.textContent="holo: "+(HOLO?"p\u00e5":"av");holoBtn.classList.toggle("active",HOLO);if(reduced)draw(0.7);});
autoBtn.addEventListener("click",toggleAuto);
cv.addEventListener("click",function(){setVariant(vi+1);});
window.addEventListener("keydown",function(e){
  if(e.key==="ArrowRight")setVariant(vi+1);
  if(e.key==="ArrowLeft")setVariant(vi-1);
  if(e.key===" "){e.preventDefault();toggleAuto();}
});

var ro=window.ResizeObserver?new ResizeObserver(function(){layout();if(reduced)draw(0.7);}):null;
if(ro)ro.observe(stage); else window.addEventListener("resize",function(){layout();if(reduced)draw(0.7);});

function frame(now){draw((now-t0)/1000);raf=requestAnimationFrame(frame);}

layout();
setVariant(0);
if(reduced){draw(0.7);} else {raf=requestAnimationFrame(frame);}
})();
</script>
</body>
</html>
Show more Show less

Pixel Glyph

A tiny 5×7 pixel font spells GPOH / CODE, then a marching-squares pass traces each glyph’s silhouette to cast that signature double neon shadow down-left. The same block grid gets re-lit four ways — a magenta/cyan plasma field, scrolling Amiga copper bars, radial hue rings, and an RGB-split glitch — each glowing through canvas shadowBlur.
🌐
_GPOH / Pixel_Glyph.html
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>GPOH · CODE — Neon 4×</title>
<style>
  :root{ --bg:#0b0b0f; }
  *{box-sizing:border-box}
  html,body{margin:0;background:var(--bg);color:#e8e8ee;
    font-family:ui-monospace,"SF Mono",Menlo,Consolas,monospace;}
  .panel{position:relative;height:clamp(320px,54vh,600px);
    border-bottom:1px solid #16161d;overflow:hidden;}
  .panel canvas{position:absolute;inset:0;width:100%;height:100%;display:block}
  .tag{position:absolute;top:14px;left:18px;z-index:2;
    font-size:11px;letter-spacing:.32em;text-transform:uppercase;
    text-shadow:0 0 14px currentColor;}
  .tag span{opacity:.5;margin-left:.7em;color:#e8e8ee;text-shadow:none}
  footer{padding:16px 18px 34px;font-size:11px;letter-spacing:.25em;
    text-transform:uppercase;color:#54545f}
</style>
</head>
<body>

  <section class="panel" data-mode="plasma">
    <div class="tag" style="color:#ff13f0">01 · Plasma <span>magenta / cyan-svep</span></div>
    <canvas></canvas>
  </section>

  <section class="panel" data-mode="copper">
    <div class="tag" style="color:#ff9d3d">02 · Kopparband <span>rullande raster</span></div>
    <canvas></canvas>
  </section>

  <section class="panel" data-mode="ripple">
    <div class="tag" style="color:#13f0ff">03 · Krusning <span>radiella färgringar</span></div>
    <canvas></canvas>
  </section>

  <section class="panel" data-mode="glitch">
    <div class="tag" style="color:#b26bff">04 · Glitch <span>kromatisk aberration</span></div>
    <canvas></canvas>
  </section>

  <footer>GPOH LABS · scrolla för att bygga varje rad på nytt · endast synlig panel beräknas</footer>

<script>
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;

/* 5×7 blocktypsnitt _gpoh */
const F = {
  C:["11111","11111","11000","11000","11000","11111","11111"],
  O:["11111","11111","11011","11011","11011","11111","11111"],
  D:["11111","11111","11011","11011","11011","11111","11111"],
  E:["11111","11111","11000","11110","11000","11111","11111"],
  G:["11111","11111","11000","11111","11011","11111","11111"],
  P:["11111","11111","11011","11011","11111","11000","11000"],
  H:["11011","11011","11011","11111","11011","11011","11011"]
};
const LINES = ["GPOH","CODE"];
const GW=5, GH=7, GAP=1, LINEGAP=2;

/* geometri, beräknas en gång _gpoh */
const glyphs=[];
LINES.forEach((word,li)=>{ let col=0; const r0=li*(GH+LINEGAP);
  for(const ch of word){ glyphs.push({c0:col,r0,bits:F[ch]}); col+=GW+GAP; }});

const cells=[]; let maxC=0,maxR=0;
for(const g of glyphs) for(let r=0;r<GH;r++) for(let c=0;c<GW;c++)
  if(g.bits[r][c]==='1'){ const gc=g.c0+c, gr=g.r0+r;
    cells.push({gc,gr}); if(gc>maxC)maxC=gc; if(gr>maxR)maxR=gr; }
const COLS=maxC+1, ROWS=maxR+1;
const cx=(COLS-1)/2, cy=(ROWS-1)/2;
for(const cl of cells){ cl.dist=Math.hypot(cl.gc-cx,cl.gr-cy); cl.rnd=Math.random(); }

const edges=[];
const fill=(g,c,r)=>c>=0&&c<GW&&r>=0&&r<GH&&g.bits[r][c]==='1';
for(const g of glyphs) for(let r=0;r<GH;r++) for(let c=0;c<GW;c++){
  if(g.bits[r][c]!=='1')continue; const x=g.c0+c,y=g.r0+r;
  if(!fill(g,c,r-1))edges.push([x,y,x+1,y]);
  if(!fill(g,c,r+1))edges.push([x,y+1,x+1,y+1]);
  if(!fill(g,c-1,r))edges.push([x,y,x,y+1]);
  if(!fill(g,c+1,r))edges.push([x+1,y,x+1,y+1]);
}

/* hjälpfunktioner _gpoh */
const clamp=(x,a,b)=>x<a?a:x>b?b:x;
const ease=x=>x<=0?0:x>=1?1:1-Math.pow(1-x,3);
const offXf=-0.42, offYf=0.72;
const MAG='#ff13f0', CYA='#13f0ff', WHT='#ffe9fb';

/* paneler _gpoh */
const dpr=Math.min(window.devicePixelRatio||1,2);
const panels=[...document.querySelectorAll('.panel')].map(sec=>{
  const cv=sec.querySelector('canvas');
  return {sec, cv, ctx:cv.getContext('2d'), mode:sec.dataset.mode,
          w:0,h:0,cell:0,ox:0,oy:0,t0:performance.now(),visible:false};
});

function layout(p){
  const r=p.cv.getBoundingClientRect();
  p.w=r.width; p.h=r.height;
  p.cv.width=Math.round(r.width*dpr); p.cv.height=Math.round(r.height*dpr);
  p.ctx.setTransform(dpr,0,0,dpr,0,0);
  p.cell=Math.max(6, Math.floor(Math.min(p.w*0.86/COLS, p.h*0.70/ROWS)));
  p.ox=Math.round((p.w-COLS*p.cell)/2);
  p.oy=Math.round((p.h-ROWS*p.cell)/2);
}
function layoutAll(){ panels.forEach(layout); if(reduce) drawStatic(); }
addEventListener('resize',layoutAll);

/* uppbyggnad per läge _gpoh */
function reveal(mode,cl,t){
  if(reduce) return 1;
  if(mode==='plasma') return ease((t-(cl.gc+cl.gr)*0.03)/0.42);
  if(mode==='copper') return ease((t-cl.gr*0.06)/0.40);
  if(mode==='ripple') return ease((t-cl.dist*0.05)/0.40);
  if(mode==='glitch') return t*1.7 > cl.rnd*1.2 ? 1 : 0;
  return 1;
}

/* blockfärg för plasma, copper och ripple _gpoh */
function style(mode,gc,gr,dist,t){
  if(mode==='plasma'){
    const v=Math.sin(gc*0.5+t*1.4)+Math.sin(gr*0.62-t)+Math.sin((gc+gr)*0.35+t*1.6);
    const hue=260+95*Math.sin(v*0.5+t*0.4);
    return {color:`hsl(${hue},100%,${clamp(56+10*Math.sin(v),44,66)}%)`, glow:0.55};
  }
  if(mode==='copper'){
    let lift=0; for(let k=0;k<3;k++){ const bp=((t*0.18+k/3)%1)*(ROWS+2)-1; const d=gr-bp; lift+=Math.exp(-(d*d)/1.4); }
    lift=Math.min(1,lift);
    return {color:`hsl(${(gr*20+t*70)%360},100%,${34+34*lift}%)`, glow:0.3+0.6*lift};
  }
  if(mode==='ripple'){
    const rip=Math.sin(dist*0.85-t*3.2);
    const hue=((dist*26-t*90)%360+360)%360;
    return {color:`hsl(${hue},100%,${clamp(52+15*rip,40,70)}%)`, glow:0.5};
  }
  return {color:'#fff',glow:0.4};
}

function contourColor(mode,t){
  if(mode==='plasma') return `hsl(${260+95*Math.sin(t*0.4)},100%,62%)`;
  if(mode==='copper') return `hsl(${(t*70)%360},100%,60%)`;
  if(mode==='ripple') return `hsl(${(t*90)%360},100%,64%)`;
  return MAG;
}

/* rita en panel _gpoh */
function draw(p,now){
  const {ctx,mode,cell,ox,oy,w,h}=p;
  const t=(now-p.t0)/1000;
  const build=clamp(t/1.6,0,1);
  const bob=reduce?0:Math.sin(t*1.1)*cell*0.03*build;
  const offX=-cell*0.42, offY=cell*0.72;

  ctx.clearRect(0,0,w,h);
  ctx.fillStyle='#0b0b0f'; ctx.fillRect(0,0,w,h);

  /* konturskugga under blocken _gpoh */
  const cA=clamp((t-0.5)/0.6,0,1);
  if(cA>0){
    ctx.globalAlpha=cA; ctx.lineCap='round'; ctx.lineJoin='round';
    ctx.lineWidth=Math.max(1.1,cell*0.06);
    const col=contourColor(mode,t);
    ctx.strokeStyle=col; ctx.shadowColor=col; ctx.shadowBlur=cell*0.4;
    for(const k of [1.0,1.5]){
      ctx.beginPath();
      for(const e of edges){
        ctx.moveTo(ox+e[0]*cell+offX*k, oy+bob+e[1]*cell+offY*k);
        ctx.lineTo(ox+e[2]*cell+offX*k, oy+bob+e[3]*cell+offY*k);
      }
      ctx.stroke();
    }
    ctx.shadowBlur=0; ctx.globalAlpha=1;
  }

  if(mode==='glitch'){
    const burst=Math.sin(t*2.3)>0.72;
    for(const cl of cells){
      const rev=reveal(mode,cl,t); if(rev<=0)continue;
      const band=Math.floor(cl.gr/2);
      const jit=burst?Math.sin(band*12.9+t*34)*cell*0.5:0;
      const x=ox+cl.gc*cell+jit, y=oy+bob+cl.gr*cell;
      const split=(1.5+3.5*Math.abs(Math.sin(t*3)))+(burst?cell*0.12:0);
      ctx.globalCompositeOperation='lighter'; ctx.globalAlpha=0.9;
      ctx.shadowBlur=cell*0.3;
      ctx.shadowColor=MAG; ctx.fillStyle=MAG; ctx.fillRect(x-split,y,cell,cell);
      ctx.shadowColor=CYA; ctx.fillStyle=CYA; ctx.fillRect(x+split,y,cell,cell);
      ctx.shadowBlur=0; ctx.globalAlpha=0.5; ctx.fillStyle=WHT; ctx.fillRect(x,y,cell,cell);
    }
    ctx.globalCompositeOperation='source-over'; ctx.globalAlpha=1;
    ctx.fillStyle='rgba(0,0,0,0.16)';
    for(let yy=0;yy<h;yy+=3) ctx.fillRect(0,yy,w,1);
    if(burst){ ctx.fillStyle='rgba(255,255,255,0.05)'; ctx.fillRect(0,(t*220)%h,w,cell*0.5); }
    return;
  }

  /* plasma / copper / ripple _gpoh */
  for(const cl of cells){
    const rev=reveal(mode,cl,t); if(rev<=0)continue;
    const st=style(mode,cl.gc,cl.gr,cl.dist,t);
    let x=ox+cl.gc*cell, y=oy+bob+cl.gr*cell, sc=1;
    if(mode==='plasma') y+=(1-rev)*cell*0.5;
    else if(mode==='copper') x+=-(1-rev)*cell*0.7;
    else if(mode==='ripple') sc=0.35+0.65*rev;
    const s=cell*sc, off=(cell-s)/2;
    ctx.globalAlpha=rev;
    ctx.shadowColor=st.color; ctx.shadowBlur=cell*st.glow;
    ctx.fillStyle=st.color; ctx.fillRect(x+off,y+off,s,s);
    ctx.shadowBlur=0; ctx.globalAlpha=rev*0.85;
    ctx.lineWidth=1; ctx.strokeStyle='rgba(0,0,0,0.30)';
    ctx.strokeRect(Math.round(x+off)+0.5,Math.round(y+off)+0.5,s-1,s-1);
  }
  ctx.globalAlpha=1; ctx.shadowBlur=0;
}

/* statisk bild vid reducerad rörelse _gpoh */
function drawStatic(){ const now=performance.now();
  panels.forEach(p=>{ p.t0=now-1600; draw(p,now); }); }

/* loop endast för synliga paneler _gpoh */
function tick(now){ for(const p of panels) if(p.visible) draw(p,now); requestAnimationFrame(tick); }

layoutAll();
if(!reduce){
  const io=new IntersectionObserver(es=>{ for(const e of es){
    const p=panels.find(x=>x.sec===e.target);
    if(e.isIntersecting){ if(!p.visible) p.t0=performance.now(); p.visible=true; }
    else p.visible=false;
  }},{threshold:0.15});
  panels.forEach(p=>io.observe(p.sec));
  requestAnimationFrame(tick);
}
</script>
</body>
</html>
Show more Show less

The Neon Dancer

The same dithered halftone figure dances through jumping-jack, groove, wave, twist and robot moves across all five versions — only the paint job changes. Version one is the plain white original with little ghosts, while the four neon variants recolor the exact same figure: glowing hue-shifting dots, a flowing plasma body, an RGB chromatic-glitch split, and a magenta-to-cyan synthwave gradient.

🌐
_GPOH / neon_dancer.html
<!--
  GPOH LABS · NEONFIGUR — samma dansande halvtonsfigur i fem skepnader _gpoh
  01 original · 02 neonpunkter · 03 plasmakropp · 04 kromatisk glitch · 05 synthvåg
  PRESTANDA: ingen shadowBlur, punktlista beräknas en gång/frame (typed arrays),
  bara synligt widget animerar (IntersectionObserver), DPR max 1.5 _gpoh
  Vanilla JS, inga byggverktyg · Elementor-HTML-widget · flerinstanssäker _gpoh
  Alla kommentarer svenska (_gpoh), alla synliga texter svenska _gpoh
-->

<style>
  /* delad stil _gpoh */
  .gpoh-cap{ font:600 13px/1.4 ui-monospace,Menlo,Consolas,monospace; letter-spacing:.18em;
    color:#FF13F0; text-transform:uppercase; margin:30px auto 8px; max-width:800px; }
  .gpoh-fig{ position:relative; width:100%; max-width:800px; margin:0 auto;
    aspect-ratio:800 / 835; overflow:hidden; line-height:0; cursor:crosshair; background:#000; }
  .gpoh-fig canvas{ display:block; width:100%; height:100%; }
  .gpoh-ttl{ position:absolute; left:18px; bottom:14px; z-index:2; pointer-events:none;
    font:800 clamp(20px,4.6vw,40px)/1 ui-monospace,monospace; letter-spacing:.04em; color:#fff;
    text-shadow:0 0 8px #FF13F0,0 0 22px #FF13F0; }
  .gpoh-sub{ position:absolute; left:18px; top:12px; z-index:2; pointer-events:none;
    font:700 12px/1.3 ui-monospace,monospace; letter-spacing:.22em; color:#19f6ff;
    text-shadow:0 0 8px #19f6ff; text-transform:uppercase; }
</style>

<script>
/* =====================================================================
   DELAD FIGUR-MOTOR — dansande halvtonsfigur, utbytbart neon-skinn _gpoh
   GPOH.figur(rot, { render, bg, post, ghosts, grid, gridColor, bgColor })
   api.forEachDot(cb) och api.paintGlow(colorFn) läser en cachead punktlista _gpoh
   ===================================================================== */
window.GPOH = window.GPOH || {};
if(!GPOH.figur){
GPOH.figur = function(root, opts){
  opts = opts || {};
  var canvas = root.querySelector('canvas'), ctx = canvas.getContext('2d');
  var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  var VW=800, VH=835, D2R=Math.PI/180, TAU=2*Math.PI;
  var PITCH=15.5, WOBBLE=4.5, AMB=0.16, DIFF=0.96, SHIM=0.05, SPRING=185, DAMP=13;
  var LSPEED=0.55, MOVEDUR=4.4, XF=0.7, MOUSE=(opts.mouseLight!==false);

  var SEG={ chestY:228,pelvisY:362,shoulderHalf:58,hipHalf:30,upArm:106,foreArm:114,
    thigh:176,shin:168,rUpArm:30,rForeArm:21,rHand:27,rThigh:40,rShin:27,rFoot:31,
    headR:41,rNeck:21,rChestTop:70,rWaist:52 };

  function lerp(a,b,t){ return a+(b-a)*t; }
  function clamp(v,a,b){ return v<a?a:(v>b?b:v); }
  function smooth(e0,e1,x){ var t=clamp((x-e0)/(e1-e0),0,1); return t*t*(3-2*t); }
  function fk(b,ang,l){ var a=ang*D2R; return {x:b.x+Math.cos(a)*l,y:b.y+Math.sin(a)*l}; }

  function base(){ return { aUL:180,aUR:0,fUL:0,fUR:0,aTL:113,aTR:67,fKL:4,fKR:-4,
    hop:0,sway:0,lean:0,headBob:0,crouch:0,ws:1.0,beat:0.0 }; }
  var KEYS=Object.keys(base());

  // dansrörelser _gpoh
  var MOVES=[
    function(lt){ var p=base(),c=0.9,ph=lt*c,s=0.5-0.5*Math.cos(TAU*ph);
      p.aUL=lerp(180,243,s);p.aUR=lerp(0,-63,s);p.aTL=lerp(113,96,s);p.aTR=lerp(67,84,s);
      p.hop=-16*Math.abs(Math.sin(TAU*ph));p.headBob=3*Math.sin(TAU*ph*2);
      p.beat=Math.pow(Math.abs(Math.sin(TAU*ph)),4);return p; },
    function(lt){ var p=base(),w=Math.sin(TAU*lt*0.6);
      p.sway=26*w;p.lean=6*w;p.aUL=150+30*Math.sin(TAU*lt*0.6+0.5);
      p.aUR=30-30*Math.sin(TAU*lt*0.6+0.5);p.aTL=100+6*w;p.aTR=80+6*w;
      p.hop=-6*Math.abs(Math.sin(TAU*lt*1.2));p.headBob=4*w;
      p.beat=Math.pow(Math.abs(Math.sin(TAU*lt*0.6)),3);return p; },
    function(lt){ var p=base();
      p.aUL=232+6*Math.sin(TAU*lt*0.7);p.aUR=-52;
      p.fUL=30*Math.sin(TAU*lt*1.4);p.fUR=30*Math.sin(TAU*lt*1.4+Math.PI);
      p.hop=-10*Math.abs(Math.sin(TAU*lt*1.4));p.aTL=104;p.aTR=76;
      p.sway=8*Math.sin(TAU*lt*0.7);p.beat=Math.pow(Math.abs(Math.sin(TAU*lt*1.4)),4);return p; },
    function(lt){ var p=base();
      p.ws=0.45+0.55*(0.5+0.5*Math.cos(TAU*lt*0.45));
      p.aUL=185+18*Math.sin(TAU*lt*0.45);p.aUR=8+18*Math.sin(TAU*lt*0.45);
      p.sway=12*Math.sin(TAU*lt*0.45);p.headBob=3*Math.sin(TAU*lt*0.9);
      p.beat=Math.pow(Math.abs(Math.sin(TAU*lt*0.45)),3);return p; },
    function(lt){ var p=base(),step=Math.floor(lt*3),s2=step%2;
      p.aUL=180;p.aUR=0;p.fUL=(s2?-70:0);p.fUR=(s2?70:0);p.aTL=108;p.aTR=72;
      p.hop=-8*s2;p.ws=0.95;p.beat=((lt*3)%1<0.15?1:0.1);return p; }
  ];
  var ci=0,pi=0,curStart=0,prevStart=0;
  function targetParams(t){
    if(t-curStart>MOVEDUR){ pi=ci;prevStart=curStart;ci=(ci+1)%MOVES.length;curStart=t; }
    var cp=MOVES[ci](t-curStart), xf=clamp((t-curStart)/XF,0,1);
    if(xf<1){ var pp=MOVES[pi](t-prevStart),bl=smooth(0,1,xf),out={};
      for(var i=0;i<KEYS.length;i++){var k=KEYS[i];out[k]=lerp(pp[k],cp[k],bl);} return out; }
    return cp;
  }

  // fjäder = wobbel _gpoh
  var sp=base(),spv={}; for(var i=0;i<KEYS.length;i++) spv[KEYS[i]]=0; var primed=false;
  function springStep(tp,dt){
    if(!primed){ for(var i=0;i<KEYS.length;i++) sp[KEYS[i]]=tp[KEYS[i]]; primed=true; return; }
    for(var i=0;i<KEYS.length;i++){ var k=KEYS[i];
      if(k==='beat'){ sp.beat+=(tp.beat-sp.beat)*Math.min(1,dt*9); continue; }
      var acc=(tp[k]-sp[k])*SPRING-spv[k]*DAMP; spv[k]+=acc*dt; sp[k]+=spv[k]*dt; }
  }

  var BODY=[],LANDMARKS=[];
  function assemble(p){
    var cx=400+p.sway, chestX=cx+p.lean;
    var chest={x:chestX,y:SEG.chestY+p.hop}, pelvis={x:cx,y:SEG.pelvisY+p.hop+p.crouch};
    var neckTop={x:chestX,y:chest.y-22}, headC={x:chestX,y:chest.y-88+p.headBob};
    var shL={x:chestX-SEG.shoulderHalf,y:chest.y-6}, shR={x:chestX+SEG.shoulderHalf,y:chest.y-6};
    var hipL={x:cx-SEG.hipHalf,y:pelvis.y}, hipR={x:cx+SEG.hipHalf,y:pelvis.y};
    var elbL=fk(shL,p.aUL,SEG.upArm),wristL=fk(elbL,p.aUL+p.fUL,SEG.foreArm);
    var elbR=fk(shR,p.aUR,SEG.upArm),wristR=fk(elbR,p.aUR+p.fUR,SEG.foreArm);
    var kneeL=fk(hipL,p.aTL,SEG.thigh),ankL=fk(kneeL,p.aTL+p.fKL,SEG.shin);
    var kneeR=fk(hipR,p.aTR,SEG.thigh),ankR=fk(kneeR,p.aTR+p.fKR,SEG.shin);
    var pts=[headC,neckTop,chest,pelvis,shL,shR,hipL,hipR,elbL,wristL,elbR,wristR,kneeL,ankL,kneeR,ankR];
    var ws=p.ws; for(var i=0;i<pts.length;i++) pts[i].x=cx+(pts[i].x-cx)*ws;
    BODY=[
      {t:'sph',cx:headC.x,cy:headC.y,r:SEG.headR},
      {t:'cap',ax:neckTop.x,ay:neckTop.y,bx:chest.x,by:chest.y,rA:SEG.rNeck,rB:SEG.rNeck+3},
      {t:'cap',ax:chest.x,ay:chest.y,bx:pelvis.x,by:pelvis.y,rA:SEG.rChestTop,rB:SEG.rWaist},
      {t:'cap',ax:shL.x,ay:shL.y,bx:elbL.x,by:elbL.y,rA:SEG.rUpArm,rB:SEG.rUpArm-6},
      {t:'cap',ax:elbL.x,ay:elbL.y,bx:wristL.x,by:wristL.y,rA:SEG.rUpArm-6,rB:SEG.rForeArm},
      {t:'cap',ax:shR.x,ay:shR.y,bx:elbR.x,by:elbR.y,rA:SEG.rUpArm,rB:SEG.rUpArm-6},
      {t:'cap',ax:elbR.x,ay:elbR.y,bx:wristR.x,by:wristR.y,rA:SEG.rUpArm-6,rB:SEG.rForeArm},
      {t:'sph',cx:wristL.x,cy:wristL.y,r:SEG.rHand},{t:'sph',cx:wristR.x,cy:wristR.y,r:SEG.rHand},
      {t:'cap',ax:hipL.x,ay:hipL.y,bx:kneeL.x,by:kneeL.y,rA:SEG.rThigh,rB:SEG.rShin+4},
      {t:'cap',ax:kneeL.x,ay:kneeL.y,bx:ankL.x,by:ankL.y,rA:SEG.rShin+4,rB:SEG.rShin},
      {t:'cap',ax:hipR.x,ay:hipR.y,bx:kneeR.x,by:kneeR.y,rA:SEG.rThigh,rB:SEG.rShin+4},
      {t:'cap',ax:kneeR.x,ay:kneeR.y,bx:ankR.x,by:ankR.y,rA:SEG.rShin+4,rB:SEG.rShin},
      {t:'sph',cx:ankL.x,cy:ankL.y,r:SEG.rFoot},{t:'sph',cx:ankR.x,cy:ankR.y,r:SEG.rFoot}
    ];
    LANDMARKS=[wristL,wristR,elbL,elbR,headC,chest,kneeL,kneeR,ankL,ankR,shL,shR];
  }

  function sample(px,py){
    var best=-1,bnx=0,bny=0,bnz=1,bcov=0;
    for(var i=0;i<BODY.length;i++){ var s=BODY[i],cov,nx,ny,nz;
      if(s.t==='sph'){ var dx=px-s.cx,dy=py-s.cy,d=Math.sqrt(dx*dx+dy*dy);
        if(d>s.r) continue; cov=1-d/s.r;nx=dx/s.r;ny=dy/s.r;nz=Math.sqrt(Math.max(0,1-nx*nx-ny*ny));
      } else { var ux=s.bx-s.ax,uy=s.by-s.ay,L2=ux*ux+uy*uy;
        var tt=L2>0?clamp(((px-s.ax)*ux+(py-s.ay)*uy)/L2,0,1):0;
        var ox=px-(s.ax+ux*tt),oy=py-(s.ay+uy*tt),d2=Math.sqrt(ox*ox+oy*oy),r=s.rA+(s.rB-s.rA)*tt;
        if(d2>r) continue; cov=1-d2/r; var ul=Math.sqrt(L2)||1,pxn=-uy/ul,pyn=ux/ul;
        var sl=clamp((ox*pxn+oy*pyn)/r,-1,1); nx=pxn*sl;ny=pyn*sl;nz=Math.sqrt(Math.max(0,1-sl*sl)); }
      if(cov>best){ best=cov;bnx=nx;bny=ny;bnz=nz;bcov=cov; } }
    if(best<0) return null;
    S_nx=bnx; S_ny=bny; S_nz=bnz; S_cov=bcov; return true;
  }
  var S_nx=0,S_ny=0,S_nz=1,S_cov=0;

  // spöken (valfritt) _gpoh
  function rnd(a,b){ return a+Math.random()*(b-a); }
  var ghostN=opts.ghosts|0, hams=[];
  for(var i=0;i<ghostN;i++) hams.push({ x:400,y:360, ai:(Math.random()*12)|0, size:rnd(44,68),
    retime:rnd(0.4,2.6), phase:rnd(0,6.28), alpha:1 });
  function updateHams(dt,t){ for(var i=0;i<hams.length;i++){ var b=hams[i]; b.retime-=dt;
    if(b.retime<=0){ b.ai=(Math.random()*LANDMARKS.length)|0; b.size=rnd(44,70); b.retime=rnd(1.8,3.6); }
    var a=LANDMARKS[b.ai]||{x:400,y:360}, ox=Math.cos(t*0.8+b.phase)*9, oy=Math.sin(t*1.2+b.phase)*8;
    b.x+=((a.x+ox)-b.x)*Math.min(1,dt*3.2); b.y+=((a.y+oy)-b.y)*Math.min(1,dt*3.2); } }
  function ghostPath(S,t){ var hw=0.34*S,tp=-0.44*S,bt=0.34*S,n=4,span=(2*hw)/n,wv=0.09*S;
    ctx.beginPath(); ctx.moveTo(-hw,bt); ctx.lineTo(-hw,tp+hw); ctx.arc(0,tp+hw,hw,Math.PI,2*Math.PI,false); ctx.lineTo(hw,bt);
    for(var i=0;i<n;i++){ var x1=hw-(i+1)*span,mx=hw-(i+0.5)*span,d=wv*(1+0.30*Math.sin(t*3+i*1.3));
      ctx.quadraticCurveTo(mx,bt+d,x1,bt); } ctx.closePath(); }
  function drawGhost(b,t,flash){ var S=b.size,bob=Math.sin(t*2+b.phase)*0.05*S-flash*0.14*S,
      sway=0.05*Math.sin(t*1.6+b.phase), sy=1+0.04*Math.sin(t*2+b.phase)+flash*0.08;
    ctx.save(); ctx.globalAlpha=b.alpha; ctx.translate(b.x,b.y+bob); ctx.rotate(sway); ctx.scale(1/Math.sqrt(sy),sy);
    ctx.save(); ctx.scale(1.14,1.14); ctx.fillStyle='#000'; ghostPath(S,t); ctx.fill(); ctx.restore();
    ctx.fillStyle='#fff'; ghostPath(S,t); ctx.fill(); ctx.fillStyle='#000';
    ctx.beginPath(); ctx.ellipse(-0.13*S,-0.08*S,0.075*S,0.10*S,0,0,6.2832); ctx.fill();
    ctx.beginPath(); ctx.ellipse( 0.13*S,-0.08*S,0.075*S,0.10*S,0,0,6.2832); ctx.fill(); ctx.restore(); }

  // ljus följer musen, annars roterar _gpoh
  var L={x:0,y:-0.1,z:1}, ptr={x:400,y:380}, ptrT=-9999;
  root.addEventListener('pointermove',function(e){ var rc=root.getBoundingClientRect();
    ptr.x=((e.clientX-rc.left)-offX)/scale; ptr.y=((e.clientY-rc.top)-offY)/scale; ptrT=performance.now(); });
  root.addEventListener('pointerleave',function(){ ptrT=-9999; });
  function lightDir(t,now){ var tx,ty,tz;
    if(MOUSE&&(now-ptrT)<1500){ tx=clamp((ptr.x-400)/340,-1,1);ty=clamp((ptr.y-380)/340,-1,1);tz=0.62; }
    else { var phi=t*LSPEED; tx=Math.sin(phi)*0.85;ty=Math.sin(phi*0.7)*0.34-0.10;tz=Math.cos(phi)*0.85+0.28; }
    var l=Math.sqrt(tx*tx+ty*ty+tz*tz)||1; tx/=l;ty/=l;tz/=l;
    L.x+=(tx-L.x)*0.12;L.y+=(ty-L.y)*0.12;L.z+=(tz-L.z)*0.12;
    var ll=Math.sqrt(L.x*L.x+L.y*L.y+L.z*L.z)||1; return {x:L.x/ll,y:L.y/ll,z:L.z/ll}; }

  // storlek/dpr (max 1.5 för fyllfart) _gpoh
  var dpr=1,scale=1,offX=0,offY=0,cssW=1,cssH=1;
  function resize(){ var w=root.clientWidth,h=root.clientHeight; if(!w||!h) return;
    dpr=Math.min(window.devicePixelRatio||1,1.5); cssW=w; cssH=h; canvas.width=Math.round(w*dpr); canvas.height=Math.round(h*dpr);
    scale=Math.min(w/VW,h/VH); offX=(w-VW*scale)/2; offY=(h-VH*scale)/2; }
  resize(); if(window.ResizeObserver) new ResizeObserver(resize).observe(root); else window.addEventListener('resize',resize);

  function drawGrid(col){ ctx.fillStyle=col||'rgba(190,194,202,0.5)'; var g=108;
    for(var y=g/2;y<VH;y+=g) for(var x=g/2;x<VW;x+=g){ ctx.beginPath(); ctx.arc(x,y,1.4,0,6.2832); ctx.fill(); } }

  // cachead punktlista (typed arrays), fylls en gång/frame _gpoh
  var MAXD=4000, DX=new Float32Array(MAXD), DY=new Float32Array(MAXD), DB=new Float32Array(MAXD), DD=new Float32Array(MAXD), dotN=0;
  var Lv={x:0,y:0,z:1};
  function computeDots(t){ dotN=0; var P=PITCH,W=WOBBLE;
    for(var gy=P*0.5;gy<VH;gy+=P){ var wy=(W*0.5)*Math.sin(gy*0.05+t*3.7);
      for(var gx=P*0.5;gx<VW;gx+=P){ var wx=W*Math.sin(gy*0.05+t*4.5);
        if(!sample(gx-wx,gy-wy)) continue;
        var diff=S_nx*Lv.x+S_ny*Lv.y+S_nz*Lv.z; if(diff<0) diff=0;
        var b=AMB+diff*DIFF; b*=smooth(0,0.40,S_cov); b+=SHIM*Math.sin(gx*0.6+gy*0.5+t*3.0);
        if(dotN<MAXD){ DX[dotN]=gx; DY[dotN]=gy; DB[dotN]=b; DD[dotN]=diff; dotN++; } } } }

  // gränssnitt till skinn _gpoh
  var api={ pitch:PITCH, clamp:clamp, smooth:smooth, VW:VW, VH:VH, t:0, flash:0, beat:0 };
  api.forEachDot=function(cb){ for(var i=0;i<dotN;i++) cb(DX[i],DY[i],DB[i],DD[i]); };
  // additiv glöd utan shadowBlur: halo-pass + kärn-pass _gpoh
  api.paintGlow=function(colorFn){ var P=PITCH,i,r,b;
    ctx.save(); ctx.globalCompositeOperation='lighter';
    ctx.globalAlpha=0.28;
    for(i=0;i<dotN;i++){ b=DB[i]; r=P*0.55*clamp(b,0,1.2); if(r<0.5) continue;
      ctx.fillStyle=colorFn(DX[i],DY[i],b,DD[i]); ctx.beginPath(); ctx.arc(DX[i],DY[i],r*2.0,0,6.2832); ctx.fill(); }
    ctx.globalAlpha=1;
    for(i=0;i<dotN;i++){ b=DB[i]; r=P*0.55*clamp(b,0,1.2); if(r<0.5) continue;
      ctx.fillStyle=colorFn(DX[i],DY[i],b,DD[i]); ctx.beginPath(); ctx.arc(DX[i],DY[i],r,0,6.2832); ctx.fill(); }
    ctx.restore(); };

  // standardskinn = vit halvton + glimt (additivt, ingen blur) _gpoh
  function classic(){ ctx.fillStyle='#fff'; var P=PITCH, hlE=0.35+0.65*api.beat+api.flash;
    for(var i=0;i<dotN;i++){ var b=DB[i],diff=DD[i],spec=Math.pow(diff,9);
      var rr=P*0.52*clamp(b,0,1.18)*(1+0.30*spec); if(rr<0.45) continue;
      ctx.beginPath();ctx.arc(DX[i],DY[i],rr,0,6.2832);ctx.fill(); }
    ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.fillStyle='rgba(200,242,255,1)';
    for(var j=0;j<dotN;j++){ var b2=DB[j],d2=DD[j],sp2=Math.pow(d2,9),g=sp2*hlE; if(g<=0.22) continue;
      var r2=P*0.52*clamp(b2,0,1.18)*(1+0.30*sp2); if(r2<=0.7) continue;
      ctx.globalAlpha=clamp(g*0.8,0,1); ctx.beginPath();ctx.arc(DX[j],DY[j],r2*1.5,0,6.2832);ctx.fill(); }
    ctx.globalAlpha=1; ctx.restore(); }

  var render = opts.render || classic;

  // ---- animering: bara när widgeten syns (IntersectionObserver) ---- _gpoh
  var visible=true, running=false, start=0, last=0;
  function frame(now){
    if(!visible){ running=false; return; }
    if(!start){start=now;last=now;}
    var t=(now-start)/1000, dt=Math.min(0.05,(now-last)/1000); last=now;
    var tp=targetParams(t); springStep(tp,dt); assemble(sp);
    api.t=t; api.flash=Math.exp(-(t-curStart)*4.5); api.beat=sp.beat;
    var lv=lightDir(t,now); Lv.x=lv.x; Lv.y=lv.y; Lv.z=lv.z;
    computeDots(t);                                   // en gång per frame _gpoh

    ctx.setTransform(dpr,0,0,dpr,0,0);
    ctx.fillStyle=opts.bgColor||'#000'; ctx.fillRect(0,0,canvas.width,canvas.height);
    ctx.translate(offX,offY); ctx.scale(scale,scale);
    if(opts.bg) opts.bg(ctx,api);
    if(opts.grid!==false) drawGrid(opts.gridColor);
    render(ctx,api);
    if(ghostN>0){ updateHams(dt,t); for(var i=0;i<hams.length;i++) drawGhost(hams[i],t,api.flash); }

    ctx.setTransform(dpr,0,0,dpr,0,0);
    if(opts.post) opts.post(ctx,{cw:cssW,ch:cssH,t:t,flash:api.flash,beat:api.beat});
    if(reduce){ running=false; return; }
    requestAnimationFrame(frame);
  }
  function kick(){ if(!running && visible){ running=true; last=0; requestAnimationFrame(frame); } }
  if(window.IntersectionObserver){
    new IntersectionObserver(function(es){ visible=es[0].isIntersecting; if(visible) kick(); },{threshold:0.01}).observe(root);
  } else { visible=true; }
  kick();
};
}
</script>


<!-- ================= VERSION 01 · ORIGINAL ================= _gpoh -->
<div class="gpoh-cap">01 · Original</div>
<div class="gpoh-fig gpoh-v01">
  <canvas></canvas>
  <span class="gpoh-sub">Vit halvton</span>
</div>
<script>
(function(){ var r=document.querySelector('.gpoh-v01:not([data-init])'); if(!r) return; r.setAttribute('data-init','1');
  GPOH.figur(r, { ghosts:7, grid:true });   // klassiskt vitt skinn med spöken _gpoh
})();
</script>


<!-- ================= VERSION 02 · NEONPUNKTER ================= _gpoh -->
<div class="gpoh-cap">02 · Neonpunkter</div>
<div class="gpoh-fig gpoh-v02" style="background:#04010a">
  <canvas></canvas>
  <span class="gpoh-sub">Glödande halvton</span>
  <span class="gpoh-ttl">NEONPUNKTER</span>
</div>
<script>
(function(){ var r=document.querySelector('.gpoh-v02:not([data-init])'); if(!r) return; r.setAttribute('data-init','1');
  GPOH.figur(r, {
    grid:true, gridColor:'rgba(255,19,240,0.16)', bgColor:'#04010a',
    render:function(ctx,api){ var t=api.t;
      // färgton vandrar över kroppen, glöd via paintGlow (ingen blur) _gpoh
      api.paintGlow(function(gx,gy,b,diff){ return 'hsl('+((gx*0.6+gy*0.4+t*70)%360)+',100%,'+(46+38*Math.min(1,diff+0.2))+'%)'; });
    },
    post:function(ctx,i){ ctx.globalAlpha=0.12; ctx.fillStyle='#000';
      for(var y=0;y<i.ch;y+=3) ctx.fillRect(0,y,i.cw,1); ctx.globalAlpha=1; }
  });
})();
</script>


<!-- ================= VERSION 03 · PLASMAKROPP ================= _gpoh -->
<div class="gpoh-cap">03 · Plasmakropp</div>
<div class="gpoh-fig gpoh-v03" style="background:#04010a">
  <canvas></canvas>
  <span class="gpoh-sub">Flödande färg</span>
  <span class="gpoh-ttl">PLASMAKROPP</span>
</div>
<script>
(function(){ var r=document.querySelector('.gpoh-v03:not([data-init])'); if(!r) return; r.setAttribute('data-init','1');
  GPOH.figur(r, {
    grid:false, bgColor:'#04010a',
    render:function(ctx,api){ var t=api.t;
      // färg ur ett plasmafält, glöd via paintGlow _gpoh
      api.paintGlow(function(gx,gy,b,diff){
        var v=Math.sin(gx*0.03+t*1.3)+Math.sin(gy*0.035-t)+Math.sin((gx+gy)*0.025+t*0.8), a=v*0.9+t*0.5;
        var R=(0.5+0.5*Math.sin(a))*255|0, G=(0.5+0.5*Math.sin(a+2.5))*255|0, B=(0.5+0.5*Math.sin(a+5.0))*255|0;
        return 'rgb('+R+','+G+','+B+')'; });
    },
    post:function(ctx,i){ ctx.globalAlpha=0.10; ctx.fillStyle='#000';
      for(var y=0;y<i.ch;y+=3) ctx.fillRect(0,y,i.cw,1); ctx.globalAlpha=1; }
  });
})();
</script>


<!-- ================= VERSION 04 · KROMATISK GLITCH ================= _gpoh -->
<div class="gpoh-cap">04 · Kromatisk glitch</div>
<div class="gpoh-fig gpoh-v04" style="background:#04010a">
  <canvas></canvas>
  <span class="gpoh-sub">RGB-splittring</span>
  <span class="gpoh-ttl">KROMATISK</span>
</div>
<script>
(function(){ var r=document.querySelector('.gpoh-v04:not([data-init])'); if(!r) return; r.setAttribute('data-init','1');
  GPOH.figur(r, {
    grid:false, bgColor:'#04010a',
    render:function(ctx,api){
      // tre förskjutna pass i R/G/B (använder cachead lista, inget extra sampling) _gpoh
      var off=3+9*api.flash+5*api.beat, P=api.pitch;
      var pass=[[-off,0,'rgba(255,20,120,1)'],[0,0,'rgba(20,255,150,1)'],[off,off*0.35,'rgba(70,120,255,1)']];
      ctx.save(); ctx.globalCompositeOperation='lighter';
      for(var k=0;k<3;k++){ ctx.save(); ctx.translate(pass[k][0],pass[k][1]); ctx.fillStyle=pass[k][2];
        api.forEachDot(function(gx,gy,b,diff){ var r=P*0.5*api.clamp(b,0,1.2); if(r<0.5) return;
          ctx.beginPath(); ctx.arc(gx,gy,r,0,6.2832); ctx.fill(); });
        ctx.restore(); }
      ctx.restore();
    },
    post:function(ctx,i){
      if(Math.sin(i.t*7)>0.86){ var y=(Math.abs(Math.sin(i.t*40))*i.ch)|0, hh=6+((i.t*100)%14);
        ctx.globalCompositeOperation='lighter'; ctx.fillStyle='rgba(25,246,255,0.35)'; ctx.fillRect(0,y,i.cw,hh);
        ctx.fillStyle='rgba(255,19,240,0.30)'; ctx.fillRect(0,y+hh,i.cw,hh*0.6); ctx.globalCompositeOperation='source-over'; }
      ctx.globalAlpha=0.12; ctx.fillStyle='#000';
      for(var y2=0;y2<i.ch;y2+=3) ctx.fillRect(0,y2,i.cw,1); ctx.globalAlpha=1;
    }
  });
})();
</script>


<!-- ================= VERSION 05 · SYNTHVÅG ================= _gpoh -->
<div class="gpoh-cap">05 · Synthvåg</div>
<div class="gpoh-fig gpoh-v05" style="background:#0a0018">
  <canvas></canvas>
  <span class="gpoh-sub">Solnedgångsdans</span>
  <span class="gpoh-ttl">SYNTHVÅG</span>
</div>
<script>
(function(){ var r=document.querySelector('.gpoh-v05:not([data-init])'); if(!r) return; r.setAttribute('data-init','1');
  GPOH.figur(r, {
    grid:false, bgColor:'#0a0018',
    bg:function(ctx,api){ // mjuk lodrät gradient bakom figuren _gpoh
      var g=ctx.createLinearGradient(0,0,0,835);
      g.addColorStop(0,'#12002b'); g.addColorStop(0.55,'#2a0140'); g.addColorStop(1,'#05010f');
      ctx.fillStyle=g; ctx.fillRect(0,0,800,835); },
    render:function(ctx,api){
      // färg = lodrät övergång magenta -> cyan, glöd via paintGlow _gpoh
      api.paintGlow(function(gx,gy,b,diff){ var f=api.clamp((gy-90)/640,0,1);
        return 'rgb('+Math.round(255+(25-255)*f)+','+Math.round(19+(246-19)*f)+','+Math.round(240+(255-240)*f)+')'; });
    },
    post:function(ctx,i){
      var yb=((i.t*90)%(i.ch+80))-40;
      var band=ctx.createLinearGradient(0,yb-30,0,yb+30);
      band.addColorStop(0,'rgba(255,255,255,0)'); band.addColorStop(0.5,'rgba(255,255,255,0.10)'); band.addColorStop(1,'rgba(255,255,255,0)');
      ctx.fillStyle=band; ctx.fillRect(0,yb-30,i.cw,60);
      ctx.globalAlpha=0.13; ctx.fillStyle='#000';
      for(var y=0;y<i.ch;y+=3) ctx.fillRect(0,y,i.cw,1); ctx.globalAlpha=1;
    }
  });
})();
</script>
Show more Show less