Cyberpunk Neon Sign: pannello arcade con Canvas, flicker e glitch (PHP + CSS + JS)
In questo articolo realizziamo un pannello "arcade cyberpunk" con un'insegna al neon disegnata su HTML5 Canvas, completa di glow multi-pass, flicker realistico, glitch cromatico, scanlines e riflesso "vetro" animato. La pagina e divisa in tre file separati: PHP (markup + testo iniziale sanitizzato), CSS (look cyberpunk) e JS (render e animazioni).
Struttura dei file
- neon-cyberpunk.php - genera la pagina e imposta il testo iniziale in modo sicuro
- neon-cyberpunk.css - costruisce pannello, cornice, scanlines, griglia "holo" e atmosfera
- neon-cyberpunk.js - disegna il neon su canvas e gestisce flicker/glitch/respirazione
Come funziona (in breve, ma serio)
- Canvas: il testo neon non e un semplice effetto CSS; viene disegnato e stratificato (profondita + glow + core bianco).
- Flicker: non e un on/off banale; usa burst e micro-drop casuali (stile neon reale).
- Glitch: brevi separazioni cromatiche (chroma split) per un look arcade aggressivo.
- Sincronizzazione: il JS aggiorna la variabile CSS
--Iper far reagire cornice e sfondo alla luminosita del neon. - Accessibilita: con
prefers-reduced-motionattivo, riduce le animazioni.
Installazione
- Crea i tre file con i nomi indicati e mettili nella stessa cartella.
- Apri neon-cyberpunk.php tramite server PHP (non via file statico), in locale o sul tuo hosting.
- Opzionale: puoi impostare un testo iniziale via URL:
?t=hello(sanitizzato, max 18 caratteri).
Personalizzazioni rapide
- Palette: modifica
--cyane--magentain neon-cyberpunk.css. - Testo default: cambia
$defaultTextin neon-cyberpunk.php. - Flicker piu aggressivo: in JS alza le probabilita in
stepEvents()(soglie tipo 0.06, 0.10, 0.22).
Codice completo
1) neon-cyberpunk.php
<?php
// neon-cyberpunk.php
// Testo iniziale (puoi cambiarlo qui)
$defaultText = "WELCOME";
// Se vuoi permettere override via URL: ?t=hello
// Sanitizzazione: solo lettere/numeri/spazi/.-_ e lunghezza max 18
$raw = isset($_GET['t']) ? (string)$_GET['t'] : '';
$raw = trim($raw);
if ($raw !== '') {
$raw = preg_replace('/[^a-zA-Z0-9 \.\-\_]/', '', $raw);
$raw = mb_substr($raw, 0, 18, 'UTF-8');
$text = strtoupper($raw);
} else {
$text = $defaultText;
}
?>
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Cyberpunk Neon Sign</title>
<link rel="stylesheet" href="neon-cyberpunk.css" />
</head>
<body>
<div id="board" data-text="<?php echo htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); ?>">
<div id="top">
<div id="screwTL" aria-hidden="true"></div>
<div id="screwTR" aria-hidden="true"></div>
<div id="screwBL" aria-hidden="true"></div>
<div id="screwBR" aria-hidden="true"></div>
<div id="plate">
<div id="hint">CYBER SIGN - Canvas Neon - Flicker + Glitch</div>
<canvas id="neonCanvas"></canvas>
<div id="grid" aria-hidden="true"></div>
<div id="glass" aria-hidden="true"></div>
</div>
</div>
<div id="bottom">
<form id="signForm" autocomplete="off">
<input id="signInput" type="text" maxlength="18" placeholder="Scrivi (max 18) e premi SEND..." />
<button type="submit">SEND</button>
</form>
</div>
</div>
<script src="neon-cyberpunk.js"></script>
</body>
</html>
2) neon-cyberpunk.css
:root{
--I: 1; /* intensita neon 0.35..1 (JS) */
--cyan: 0,255,255;
--magenta: 255,0,255;
--bg0: #03010a;
--bg1: #070a18;
}
html,body{height:100%}
body{
margin:0;
display:grid;
place-items:center;
padding:24px;
color:#fff;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
overflow:hidden;
background:
radial-gradient(1200px 700px at 50% 10%, rgba(120,40,255,0.22), rgba(0,0,0,0) 60%),
radial-gradient(900px 600px at 20% 80%, rgba(0,255,255,0.10), rgba(0,0,0,0) 55%),
radial-gradient(900px 600px at 85% 75%, rgba(255,0,255,0.10), rgba(0,0,0,0) 55%),
linear-gradient(120deg, var(--bg0), var(--bg1));
}
body::before{
content:"";
position:fixed;
inset:-40%;
background:
conic-gradient(from 180deg at 50% 50%,
rgba(0,255,255,0.05),
rgba(255,0,255,0.06),
rgba(0,255,255,0.05));
filter: blur(60px);
opacity: 0.7;
animation: haze 10s linear infinite;
pointer-events:none;
}
@keyframes haze{ to{ transform: rotate(360deg) } }
/* ===== BOARD ===== */
#board{
width:min(820px, 94vw);
border-radius:18px;
overflow:hidden;
position:relative;
transform: perspective(1200px) rotateX(1.3deg);
border:1px solid rgba(255,255,255,0.08);
background: linear-gradient(135deg, #11131a, #080a0f 55%, #0f121a);
box-shadow:
0 35px 90px rgba(0,0,0,0.70),
0 0 calc(45px * var(--I)) rgba(var(--cyan), 0.14),
0 0 calc(28px * var(--I)) rgba(var(--magenta), 0.12);
}
#board::before{
content:"";
position:absolute;
inset:8px;
border-radius:14px;
pointer-events:none;
box-shadow:
0 0 calc(26px * var(--I)) rgba(var(--cyan), 0.25),
0 0 calc(18px * var(--I)) rgba(var(--magenta), 0.18),
inset 0 0 0 1px rgba(255,255,255,0.06),
inset 0 18px 24px rgba(255,255,255,0.04),
inset 0 -22px 30px rgba(0,0,0,0.60);
mix-blend-mode: screen;
opacity: 0.9;
}
/* scanlines overlay */
#board::after{
content:"";
position:absolute;
inset:0;
pointer-events:none;
background:
repeating-linear-gradient(
0deg,
rgba(255,255,255,0.020) 0px,
rgba(255,255,255,0.020) 1px,
rgba(0,0,0,0) 4px,
rgba(0,0,0,0) 9px
),
radial-gradient(900px 260px at 50% 0%, rgba(255,255,255,0.08), rgba(0,0,0,0) 60%),
radial-gradient(700px 220px at 50% 110%, rgba(var(--cyan), 0.08), rgba(0,0,0,0) 60%);
opacity: 0.70;
animation: scan 2.2s linear infinite;
}
@keyframes scan{
0%{ transform: translateY(-2px) }
100%{ transform: translateY(2px) }
}
#top{
position:relative;
padding:18px;
background:
radial-gradient(1200px 260px at 50% 0%, rgba(255,255,255,0.08), rgba(255,255,255,0) 55%),
linear-gradient(180deg, #0b0d13, #070910);
border-bottom: 1px solid rgba(255,255,255,0.08);
}
/* screws */
#screwTL,#screwTR,#screwBL,#screwBR{
position:absolute;
width:14px;height:14px;border-radius:50%;
background:
radial-gradient(circle at 30% 30%, rgba(255,255,255,0.35), rgba(255,255,255,0) 45%),
radial-gradient(circle at 50% 55%, #2b2f35 0%, #15181c 60%, #0b0d10 100%);
border:1px solid rgba(255,255,255,0.08);
box-shadow:
inset 0 1px 1px rgba(255,255,255,0.15),
inset 0 -1px 2px rgba(0,0,0,0.7),
0 2px 6px rgba(0,0,0,0.6);
opacity:0.9;
}
#screwTL{top:10px;left:10px}
#screwTR{top:10px;right:10px}
#screwBL{bottom:10px;left:10px}
#screwBR{bottom:10px;right:10px}
/* plate */
#plate{
position:relative;
border-radius:14px;
overflow:hidden;
border:1px solid rgba(255,255,255,0.08);
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.06),
inset 0 -22px 34px rgba(0,0,0,0.60),
0 12px 28px rgba(0,0,0,0.60),
0 0 calc(24px * var(--I)) rgba(var(--cyan), 0.18),
0 0 calc(20px * var(--I)) rgba(var(--magenta), 0.14);
background:
radial-gradient(700px 180px at 50% 45%, rgba(255,255,255,0.06), rgba(255,255,255,0) 65%),
repeating-linear-gradient(
0deg,
rgba(255,255,255,0.018) 0px,
rgba(255,255,255,0.018) 1px,
rgba(0,0,0,0) 3px,
rgba(0,0,0,0) 7px
),
linear-gradient(180deg, #090a10, #04050a);
}
#hint{
position:absolute;
top:10px; left:14px; right:14px;
font-size:12px;
opacity:0.65;
text-align:left;
letter-spacing:0.2px;
user-select:none;
z-index:3;
}
#neonCanvas{
display:block;
width:100%;
height:140px;
}
/* glass sweep */
#glass{
position:absolute;
inset:0;
pointer-events:none;
background:
linear-gradient(115deg,
rgba(255,255,255,0.18) 0%,
rgba(255,255,255,0.06) 18%,
rgba(255,255,255,0.00) 42%,
rgba(255,255,255,0.10) 62%,
rgba(255,255,255,0.00) 88%
);
mix-blend-mode: screen;
opacity:0.82;
transform: translateX(-35%);
animation: sweep 3.2s ease-in-out infinite;
filter: blur(0.25px);
}
@keyframes sweep{
0% { transform: translateX(-45%) skewX(-10deg) }
50% { transform: translateX( 25%) skewX(-10deg) }
100% { transform: translateX(-45%) skewX(-10deg) }
}
/* holo grid */
#grid{
position:absolute;
inset:0;
pointer-events:none;
background:
linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.55)),
repeating-linear-gradient(90deg, rgba(var(--cyan),0.05) 0 1px, rgba(0,0,0,0) 1px 34px),
repeating-linear-gradient(0deg, rgba(var(--magenta),0.04) 0 1px, rgba(0,0,0,0) 1px 28px);
opacity: calc(0.35 + 0.25 * var(--I));
mix-blend-mode: screen;
animation: drift 7s linear infinite;
}
@keyframes drift{ to{ transform: translate(28px, 22px) } }
/* bottom haze + form */
#bottom{
position:relative;
height:320px;
background:
radial-gradient(900px 260px at 50% 0%,
rgba(var(--cyan), calc(0.10 + 0.30 * var(--I))) 0%,
rgba(var(--magenta), calc(0.06 + 0.18 * var(--I))) 40%,
rgba(0,0,0,0) 65%
),
linear-gradient(180deg,
rgba(var(--cyan), calc(0.35 + 0.55 * var(--I))) 0%,
rgba(10,10,14,0.65) 70%,
rgba(0,0,0,0.80) 100%
);
box-shadow: inset 0 18px 30px rgba(0,0,0,0.35);
}
#bottom::before{
content:"";
position:absolute;
inset:0;
pointer-events:none;
background:
radial-gradient(800px 260px at 50% 20%, rgba(255,255,255,0.06), rgba(0,0,0,0) 60%),
repeating-linear-gradient(0deg, rgba(255,255,255,0.016) 0 1px, rgba(0,0,0,0) 1px 10px);
opacity: 0.7;
}
#signForm{
position:absolute;
left:5%;
width:90%;
bottom:22px;
display:flex;
gap:10px;
z-index:2;
}
#signInput, #signForm button{
font-size:16px;
padding:12px 14px;
border-radius:12px;
border:1px solid rgba(255,255,255,0.10);
color:#fff;
background: rgba(0,0,0,0.22);
outline:none;
box-shadow: inset 0 10px 20px rgba(0,0,0,0.35);
}
#signInput{
flex:1;
}
#signInput:focus{
border-color: rgba(var(--cyan), 0.45);
box-shadow:
inset 0 10px 20px rgba(0,0,0,0.35),
0 0 20px rgba(var(--cyan), 0.18),
0 0 18px rgba(var(--magenta), 0.10);
}
#signForm button{
cursor:pointer;
background: linear-gradient(180deg, rgba(255,255,255,0.12), rgba(0,0,0,0.25));
box-shadow: 0 12px 28px rgba(0,0,0,0.40);
transition: transform .15s ease, filter .15s ease;
white-space:nowrap;
}
#signForm button:hover{
transform: translateY(-1px);
filter: brightness(1.08);
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce){
body::before, #glass, #grid, #board::after{ animation: none !important; }
#board{ transform:none; }
}
3) neon-cyberpunk.js
const board = document.getElementById("board");
const canvas = document.getElementById("neonCanvas");
const ctx = canvas.getContext("2d");
const prefersReduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let neonText = (board.getAttribute("data-text") || "WELCOME").toUpperCase().slice(0,18);
let I = 1;
let targetI = 1;
let glitch = 0;
let nextEvent = 0;
function clamp(x,a,b){ return Math.max(a, Math.min(b,x)); }
function setupHiDPI(){
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
const cssW = Math.max(320, Math.round(rect.width));
const cssH = Math.max(120, Math.round(rect.height));
canvas.width = Math.round(cssW * dpr);
canvas.height = Math.round(cssH * dpr);
// Disegno in coordinate CSS
ctx.setTransform(dpr,0,0,dpr,0,0);
return { W: cssW, H: cssH };
}
let dims = setupHiDPI();
window.addEventListener("resize", () => { dims = setupHiDPI(); });
function stepEvents(){
if (prefersReduced){
targetI = 1;
glitch = 0;
return;
}
const r = Math.random();
// glitch cromatico breve (arcade)
if (r < 0.06){
glitch = 1;
setTimeout(() => { glitch = 0; }, 60 + Math.random()*90);
}
// flicker: burst o drop
if (r < 0.10){
const hits = 3 + Math.floor(Math.random()*4); // 3-6
let k = 0;
const burst = () => {
const off = (k % 2 === 0);
targetI = off ? 0.42 : 1;
k++;
if (k < hits) setTimeout(burst, 28 + Math.random()*45);
else targetI = 1;
};
burst();
} else if (r < 0.22){
targetI = 0.45;
setTimeout(() => { targetI = 1; }, 55 + Math.random()*90);
} else {
targetI = 0.88 + Math.random()*0.12;
}
}
function draw(){
const W = dims.W, H = dims.H;
ctx.clearRect(0,0,W,H);
// vignette interna
const g = ctx.createRadialGradient(W*0.5,H*0.5, 30, W*0.5,H*0.5, W*0.62);
g.addColorStop(0, "rgba(255,255,255,0.04)");
g.addColorStop(1, "rgba(0,0,0,0.45)");
ctx.fillStyle = g;
ctx.fillRect(0,0,W,H);
const cx = W/2, cy = H/2 + 2;
const fontSize = Math.round(Math.min(74, Math.max(46, W/12)));
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = `900 ${fontSize}px Arial`;
// profondita pseudo-3D
const depth = 10;
for (let i=depth; i>0; i--){
const k = i/depth;
ctx.fillStyle = `rgba(0,0,0,${0.48 + 0.40*k})`;
ctx.fillText(neonText, cx + i*1.25, cy + i*1.15);
}
// glow esterno multipass (cyan + magenta)
ctx.save();
ctx.globalCompositeOperation = "lighter";
ctx.shadowBlur = 32 * I;
ctx.shadowColor = "rgba(0,255,255,1)";
for (let w=18; w>=8; w-=5){
ctx.lineWidth = w;
ctx.strokeStyle = `rgba(0,255,255,${0.18*I})`;
ctx.strokeText(neonText, cx, cy);
}
ctx.shadowColor = "rgba(255,0,255,1)";
ctx.shadowBlur = 26 * I;
for (let w=16; w>=7; w-=4){
ctx.lineWidth = w;
ctx.strokeStyle = `rgba(255,0,255,${0.12*I})`;
ctx.strokeText(neonText, cx, cy);
}
ctx.restore();
// corpo tubo
ctx.save();
ctx.shadowBlur = 26 * I;
ctx.shadowColor = "rgba(0,255,255,1)";
ctx.fillStyle = `rgba(0,255,255,${0.70*I})`;
ctx.fillText(neonText, cx, cy);
ctx.restore();
// core bianco
ctx.save();
ctx.globalCompositeOperation = "screen";
ctx.shadowBlur = 12 * I;
ctx.shadowColor = "rgba(255,255,255,1)";
ctx.fillStyle = `rgba(255,255,255,${0.55*I})`;
ctx.font = `900 ${Math.max(10, fontSize-2)}px Arial`;
ctx.fillText(neonText, cx, cy);
ctx.restore();
// outline
ctx.save();
ctx.lineWidth = 2;
ctx.strokeStyle = `rgba(0,255,255,${0.95*I})`;
ctx.strokeText(neonText, cx, cy);
ctx.restore();
// glitch cromatico (chroma split)
if (glitch > 0){
const dx = (Math.random()*10 - 5) * glitch;
const dy = (Math.random()*4 - 2) * glitch;
ctx.save();
ctx.globalCompositeOperation = "screen";
ctx.font = `900 ${fontSize}px Arial`;
ctx.fillStyle = `rgba(255,0,255,${0.30*I})`;
ctx.fillText(neonText, cx + dx, cy + dy);
ctx.fillStyle = `rgba(0,255,255,${0.30*I})`;
ctx.fillText(neonText, cx - dx*0.8, cy - dy*0.8);
ctx.fillStyle = `rgba(255,255,255,${0.15*I})`;
ctx.fillText(neonText, cx + dx*0.3, cy - dy*0.3);
ctx.restore();
}
// scintille
if (!prefersReduced && Math.random() < 0.10 * (1 - I + 0.15)){
const sx = cx + (Math.random()*W*0.7 - W*0.35);
const sy = cy + (Math.random()*H*0.35 - H*0.18);
ctx.save();
ctx.globalCompositeOperation = "lighter";
ctx.fillStyle = `rgba(255,255,255,${0.22*I})`;
ctx.shadowBlur = 14*I;
ctx.shadowColor = "rgba(0,255,255,1)";
ctx.beginPath();
ctx.arc(sx, sy, 1.2 + Math.random()*1.4, 0, Math.PI*2);
ctx.fill();
ctx.restore();
}
}
function tick(ts){
// eventi ogni ~120-280ms
if (!prefersReduced && ts > nextEvent){
stepEvents();
nextEvent = ts + (120 + Math.random()*220);
}
// smoothing intensita (inerzia)
I += (targetI - I) * 0.25;
I = clamp(I, 0.35, 1);
// esporta a CSS var
board.style.setProperty("--I", I.toFixed(3));
draw();
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
// Form: cambia testo
document.getElementById("signForm").addEventListener("submit", (e) => {
e.preventDefault();
const inp = document.getElementById("signInput");
const v = (inp.value || "").trim();
if (!v) return;
neonText = v.toUpperCase().slice(0,18);
inp.value = "";
// colpo di luce + glitch breve
targetI = 1;
if (!prefersReduced){
glitch = 1;
setTimeout(() => { glitch = 0; }, 90);
}
});
Chiusura
Questo pannello non e un "effettino": e una mini-interfaccia cyberpunk completa, riutilizzabile in hero section, pagine demo o componenti UI. La separazione PHP/CSS/JS mantiene il progetto pulito e pronto da integrare nel tuo flusso editoriale.