Files
QR-master/qr-design-showcase.html

1045 lines
39 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QR Code Design Showcase</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcode-generator/1.4.4/qrcode.js"></script>
<style>
:root{
--bg:#0f1115;
--panel:#171a21;
--panel2:#1e2129;
--border:#2a2e38;
--text:#eef0f4;
--muted:#9aa2b1;
--accent:#7c5cff;
--accent2:#00d4b5;
}
*{box-sizing:border-box;}
body{
margin:0;
background:linear-gradient(160deg,#0f1115,#14161d 60%,#0f1115);
color:var(--text);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
padding:32px 20px 80px;
}
header{
max-width:1200px;
margin:0 auto 28px;
text-align:center;
}
header h1{
font-size:28px;
margin:0 0 8px;
background:linear-gradient(90deg,var(--accent),var(--accent2));
-webkit-background-clip:text;
background-clip:text;
color:transparent;
font-weight:800;
}
header p{
color:var(--muted);
margin:0;
font-size:14px;
}
.customizer{
max-width:1200px;
margin:0 auto 40px;
background:var(--panel);
border:1px solid var(--border);
border-radius:16px;
padding:22px;
display:grid;
grid-template-columns:380px 1fr;
gap:24px;
}
@media (max-width:860px){
.customizer{grid-template-columns:1fr;}
}
.group{
border:1px solid var(--border);
border-radius:12px;
padding:14px 14px 4px;
margin-bottom:14px;
background:rgba(255,255,255,.015);
}
.group-title{
font-size:11px;
text-transform:uppercase;
letter-spacing:.08em;
color:var(--accent2);
font-weight:700;
margin-bottom:10px;
}
.field{margin-bottom:14px;}
.field label{
display:flex;
justify-content:space-between;
font-size:12px;
text-transform:uppercase;
letter-spacing:.06em;
color:var(--muted);
margin-bottom:6px;
}
.field label span.val{ color:var(--accent2); text-transform:none; letter-spacing:0; font-weight:600;}
.field input[type=text], .field select{
width:100%;
background:var(--panel2);
border:1px solid var(--border);
color:var(--text);
padding:9px 10px;
border-radius:8px;
font-size:14px;
}
.field input[type=color]{
width:100%;
height:38px;
border:1px solid var(--border);
border-radius:8px;
background:var(--panel2);
padding:2px;
}
.field input[type=range]{
width:100%;
accent-color: var(--accent);
}
.checkline{
display:flex;
align-items:center;
gap:8px;
font-size:13px;
color:var(--text);
margin-bottom:10px;
}
.checkline input{width:16px;height:16px;}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;}
.preview-wrap{
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
gap:14px;
position:sticky;
top:20px;
}
.preview-card{
background:#fff;
border-radius:18px;
padding:20px;
box-shadow:0 10px 40px rgba(0,0,0,.35);
display:flex;
align-items:center;
justify-content:center;
}
#liveCanvas{display:block;border-radius:6px; max-width:100%; height:auto;}
.btn-row{display:flex; gap:10px; flex-wrap:wrap; justify-content:center;}
.dl-btn{
background:var(--accent);
color:#fff;
border:none;
padding:10px 18px;
border-radius:8px;
font-size:13px;
font-weight:600;
cursor:pointer;
}
.dl-btn.secondary{
background:var(--panel2);
border:1px solid var(--border);
color:var(--text);
}
.dl-btn:hover{filter:brightness(1.15);}
h2.section-title{
max-width:1200px;
margin:40px auto 16px;
font-size:18px;
color:var(--text);
display:flex;
align-items:center;
gap:10px;
}
h2.section-title .badge{
font-size:11px;
background:var(--panel2);
border:1px solid var(--border);
color:var(--muted);
padding:3px 9px;
border-radius:20px;
font-weight:500;
}
.gallery{
max-width:1200px;
margin:0 auto;
display:grid;
grid-template-columns:repeat(auto-fill,minmax(220px,1fr));
gap:18px;
}
.card{
background:var(--panel);
border:1px solid var(--border);
border-radius:14px;
padding:16px;
display:flex;
flex-direction:column;
align-items:center;
gap:10px;
transition:transform .15s ease, border-color .15s ease;
cursor:pointer;
}
.card:hover{
transform:translateY(-3px);
border-color:var(--accent);
}
.card .swatch{
background:#fff;
border-radius:10px;
padding:10px;
display:flex;
align-items:center;
justify-content:center;
}
.card canvas{display:block; max-width:100%; height:auto;}
.card .name{
font-size:13px;
font-weight:600;
text-align:center;
}
.card .desc{
font-size:11px;
color:var(--muted);
text-align:center;
line-height:1.4;
}
footer{
max-width:1200px;
margin:50px auto 0;
text-align:center;
color:var(--muted);
font-size:12px;
}
</style>
</head>
<body>
<header>
<h1>QR Code Design Showcase</h1>
<p>Ein Baukasten für QR-Code-Designs — Modulformen, Ecken-Stile, Farbverläufe, Rahmen, Logos & mehr. Klick auf eine Karte, um sie in den Editor zu laden.</p>
</header>
<div class="customizer">
<div>
<div class="group">
<div class="group-title">Inhalt</div>
<div class="field">
<label>Inhalt / Link</label>
<input type="text" id="dataInput" value="https://qrmaster.com" />
</div>
<div class="field">
<label>Fehlerkorrektur</label>
<select id="ecLevel">
<option value="L">L (7%)</option>
<option value="M" selected>M (15%)</option>
<option value="Q">Q (25%)</option>
<option value="H">H (30%) — empfohlen bei Logo</option>
</select>
</div>
</div>
<div class="group">
<div class="group-title">Formen</div>
<div class="row2">
<div class="field">
<label>Modul-Form</label>
<select id="bodyShape">
<option value="square">Eckig (klassisch)</option>
<option value="rounded" selected>Abgerundet</option>
<option value="dots">Punkte / Dots</option>
<option value="classy">Classy (fließend)</option>
<option value="classy-rounded">Classy Rounded</option>
<option value="liquid">Liquid / Blob</option>
<option value="diamond">Diamant</option>
<option value="star">Sterne</option>
<option value="hexagon">Hexagon</option>
<option value="plus">Plus / Kreuz</option>
<option value="mosaic">Mosaic (gemischt)</option>
</select>
</div>
<div class="field">
<label>Ecken-Rahmen (Eyes)</label>
<select id="eyeFrame">
<option value="square">Eckig</option>
<option value="rounded" selected>Abgerundet</option>
<option value="circle">Kreis</option>
<option value="leaf">Blatt</option>
<option value="flower">Flower / Squircle</option>
<option value="hexagon">Hexagon</option>
</select>
</div>
</div>
<div class="field">
<label>Ecken-Kern (Eye Ball)</label>
<select id="eyeBall">
<option value="square">Eckig</option>
<option value="rounded">Abgerundet</option>
<option value="circle" selected>Kreis</option>
<option value="diamond">Diamant</option>
<option value="star">Stern</option>
<option value="hexagon">Hexagon</option>
</select>
</div>
</div>
<div class="group">
<div class="group-title">Farben</div>
<div class="field">
<label>Farb-Modus</label>
<select id="colorMode">
<option value="solid">Einfarbig</option>
<option value="linear" selected>Verlauf (linear)</option>
<option value="radial">Verlauf (radial)</option>
</select>
</div>
<div class="row2">
<div class="field">
<label>Farbe 1</label>
<input type="color" id="color1" value="#7c5cff" />
</div>
<div class="field">
<label>Farbe 2</label>
<input type="color" id="color2" value="#00d4b5" />
</div>
</div>
<div class="field" id="angleField">
<label>Verlaufswinkel <span class="val" id="angleVal">45°</span></label>
<input type="range" id="gradAngle" min="0" max="359" value="45" />
</div>
<label class="checkline">
<input type="checkbox" id="eyeColorToggle" />
Eigene Farbe für die Ecken (Eyes)
</label>
<div class="row2" id="eyeColorFields" style="display:none;">
<div class="field">
<label>Eye-Farbe 1</label>
<input type="color" id="eyeColor1" value="#111111" />
</div>
<div class="field">
<label>Eye-Farbe 2</label>
<input type="color" id="eyeColor2" value="#111111" />
</div>
</div>
<div class="field">
<label>Hintergrund</label>
<input type="color" id="bgColor" value="#ffffff" />
</div>
</div>
<div class="group">
<div class="group-title">Rahmen &amp; Label</div>
<div class="field">
<label>Rahmen-Stil</label>
<select id="frameStyle">
<option value="none">Kein Rahmen</option>
<option value="card">Weiße Karte mit Schatten</option>
<option value="label-bottom">Karte + Label unten</option>
<option value="label-top">Karte + Label oben</option>
</select>
</div>
<div class="row2" id="labelFields" style="display:none;">
<div class="field">
<label>Label-Text</label>
<input type="text" id="labelText" value="SCAN MICH" />
</div>
<div class="field">
<label>Label-Farbe</label>
<input type="color" id="labelColor" value="#111111" />
</div>
</div>
</div>
<div class="group">
<div class="group-title">Logo in der Mitte</div>
<div class="field">
<label>Logo-Symbol</label>
<select id="logoShape">
<option value="none">Kein Logo</option>
<option value="dot">Punkt</option>
<option value="square">Quadrat</option>
<option value="heart">Herz</option>
<option value="bolt">Blitz</option>
<option value="wifi">WLAN-Symbol</option>
</select>
</div>
<div class="row2">
<div class="field">
<label>Logo-Farbe</label>
<input type="color" id="logoColor" value="#111111" />
</div>
<div class="field">
<label>Logo-Hintergrund</label>
<input type="color" id="logoBg" value="#ffffff" />
</div>
</div>
</div>
<div class="group">
<div class="group-title">Feintuning</div>
<div class="row3">
<div class="field">
<label>Modulgröße <span class="val" id="moduleVal">8</span></label>
<input type="range" id="moduleSize" min="4" max="14" value="8" />
</div>
<div class="field">
<label>Weißraum <span class="val" id="quietVal">2</span></label>
<input type="range" id="quietZone" min="0" max="6" value="2" />
</div>
<div class="field">
<label>Rotation <span class="val" id="rotVal"></span></label>
<input type="range" id="rotation" min="0" max="359" value="0" />
</div>
</div>
</div>
</div>
<div class="preview-wrap">
<div class="preview-card">
<canvas id="liveCanvas" width="420" height="420"></canvas>
</div>
<div class="btn-row">
<button class="dl-btn" id="downloadBtn">PNG herunterladen</button>
<button class="dl-btn secondary" id="randomBtn">🎲 Zufalls-Design</button>
</div>
</div>
</div>
<h2 class="section-title">Design-Galerie <span class="badge" id="galleryCount">Varianten — anklicken zum Übernehmen</span></h2>
<div class="gallery" id="gallery"></div>
<footer>QR Master · Design Showcase · reine Frontend-Demo, alle Codes sind scanbar</footer>
<script>
/* ---------- QR matrix helper ---------- */
function getMatrix(text, ec){
const qr = qrcode(0, ec || 'M');
qr.addData(text || ' ');
qr.make();
const n = qr.getModuleCount();
const matrix = [];
for(let r=0;r<n;r++){
const row = [];
for(let c=0;c<n;c++) row.push(qr.isDark(r,c));
matrix.push(row);
}
return matrix;
}
function isInEye(r,c,n){
const eyes = [[0,0],[0,n-7],[n-7,0]];
return eyes.some(([er,ec])=> r>=er && r<er+7 && c>=ec && c<ec+7);
}
/* simple deterministic hash for mosaic variety */
function cellHash(r,c){
const v = Math.sin(r*127.1 + c*311.7) * 43758.5453;
return v - Math.floor(v);
}
/* ---------- shape drawing primitives ---------- */
function variableRoundRect(ctx,x,y,w,h,tl,tr,br,bl){
ctx.moveTo(x+tl,y);
ctx.lineTo(x+w-tr,y);
if(tr) ctx.arcTo(x+w,y,x+w,y+tr,tr); else ctx.lineTo(x+w,y);
ctx.lineTo(x+w,y+h-br);
if(br) ctx.arcTo(x+w,y+h,x+w-br,y+h,br); else ctx.lineTo(x+w,y+h);
ctx.lineTo(x+bl,y+h);
if(bl) ctx.arcTo(x,y+h,x,y+h-bl,bl); else ctx.lineTo(x,y+h);
ctx.lineTo(x,y+tl);
if(tl) ctx.arcTo(x,y,x+tl,y,tl); else ctx.lineTo(x,y);
ctx.closePath();
}
function hexagonPath(ctx,cx,cy,r){
for(let i=0;i<6;i++){
const angle = Math.PI/180*(60*i - 30);
const px = cx + r*Math.cos(angle);
const py = cy + r*Math.sin(angle);
if(i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py);
}
ctx.closePath();
}
function starPath(ctx,cx,cy,outerR,innerR,spikes){
let rot = Math.PI/2*3;
const step = Math.PI/spikes;
ctx.moveTo(cx, cy-outerR);
for(let i=0;i<spikes;i++){
ctx.lineTo(cx+Math.cos(rot)*outerR, cy+Math.sin(rot)*outerR); rot+=step;
ctx.lineTo(cx+Math.cos(rot)*innerR, cy+Math.sin(rot)*innerR); rot+=step;
}
ctx.closePath();
}
function drawBodyModule(ctx, x, y, size, shape, neighbors, r, c){
const {top,bottom,left,right} = neighbors;
ctx.beginPath();
switch(shape){
case 'square': {
ctx.rect(x,y,size,size);
break;
}
case 'dots': {
ctx.arc(x+size/2,y+size/2,size*0.42,0,Math.PI*2);
break;
}
case 'diamond': {
ctx.moveTo(x+size/2,y);
ctx.lineTo(x+size,y+size/2);
ctx.lineTo(x+size/2,y+size);
ctx.lineTo(x,y+size/2);
ctx.closePath();
break;
}
case 'star': {
starPath(ctx, x+size/2, y+size/2, size*0.5, size*0.22, 5);
break;
}
case 'hexagon': {
hexagonPath(ctx, x+size/2, y+size/2, size*0.52);
break;
}
case 'plus': {
const armW = size*0.42;
const off = (size-armW)/2;
ctx.rect(x+off, y, armW, size);
ctx.rect(x, y+off, size, armW);
break;
}
case 'mosaic': {
const h = cellHash(r,c);
if(h < 0.34){
ctx.arc(x+size/2,y+size/2,size*0.42,0,Math.PI*2);
} else if(h < 0.67){
const rMed = size*0.28;
variableRoundRect(ctx,x,y,size,size,rMed,rMed,rMed,rMed);
} else {
ctx.rect(x,y,size,size);
}
break;
}
case 'liquid': {
const rBig = size*0.55;
const tl = (!top && !left) ? rBig : 0;
const tr = (!top && !right) ? rBig : 0;
const br = (!bottom && !right) ? rBig : 0;
const bl = (!bottom && !left) ? rBig : 0;
variableRoundRect(ctx,x,y,size,size,tl,tr,br,bl);
break;
}
case 'classy': {
const rBig = size*0.5;
const tl = (!top && !left) ? rBig : 0;
const tr = (!top && !right) ? rBig : 0;
const br = (!bottom && !right) ? rBig : 0;
const bl = (!bottom && !left) ? rBig : 0;
variableRoundRect(ctx,x,y,size,size,tl,tr,br,bl);
break;
}
case 'classy-rounded': {
const rBig = size*0.5;
const tl = (!top && !left) ? rBig : (top&&left? 0: rBig*0.35);
const tr = (!top && !right) ? rBig : (top&&right? 0: rBig*0.35);
const br = (!bottom && !right) ? rBig : (bottom&&right? 0: rBig*0.35);
const bl = (!bottom && !left) ? rBig : (bottom&&left? 0: rBig*0.35);
variableRoundRect(ctx,x,y,size,size,tl,tr,br,bl);
break;
}
case 'rounded':
default: {
const rMed = size*0.32;
const tl = (!top && !left) ? rMed : 0;
const tr = (!top && !right) ? rMed : 0;
const br = (!bottom && !right) ? rMed : 0;
const bl = (!bottom && !left) ? rMed : 0;
variableRoundRect(ctx,x,y,size,size,tl,tr,br,bl);
break;
}
}
ctx.fill();
}
function drawEyeFrame(ctx,x,y,size,shape){
const s = size;
ctx.beginPath();
if(shape==='circle'){
ctx.arc(x+s/2,y+s/2,s/2,0,Math.PI*2);
ctx.arc(x+s/2,y+s/2,s/2*(5/7),0,Math.PI*2,true);
} else if(shape==='hexagon'){
hexagonPath(ctx, x+s/2, y+s/2, s/2);
hexagonPath(ctx, x+s/2, y+s/2, s/2*(5/7));
} else if(shape==='leaf'){
variableRoundRect(ctx,x,y,s,s, s*0.45,0,s*0.45,0);
variableRoundRect(ctx,x+s*(1/7),y+s*(1/7), s*(5/7), s*(5/7), s*0.32*(5/7),0,s*0.32*(5/7),0);
} else if(shape==='flower'){
variableRoundRect(ctx,x,y,s,s, s*0.42,s*0.42,s*0.42,s*0.42);
variableRoundRect(ctx,x+s*(1/7),y+s*(1/7), s*(5/7), s*(5/7), s*0.4*(5/7),s*0.4*(5/7),s*0.4*(5/7),s*0.4*(5/7));
} else if(shape==='rounded'){
variableRoundRect(ctx,x,y,s,s,s*0.28,s*0.28,s*0.28,s*0.28);
variableRoundRect(ctx,x+s*(1/7),y+s*(1/7), s*(5/7), s*(5/7), s*0.22,s*0.22,s*0.22,s*0.22);
} else {
ctx.rect(x,y,s,s);
ctx.rect(x+s*(1/7),y+s*(1/7), s*(5/7), s*(5/7));
}
ctx.fill('evenodd');
}
function drawEyeBall(ctx,x,y,size,shape){
ctx.beginPath();
if(shape==='circle'){
ctx.arc(x+size/2,y+size/2,size/2,0,Math.PI*2);
} else if(shape==='diamond'){
ctx.moveTo(x+size/2,y);
ctx.lineTo(x+size,y+size/2);
ctx.lineTo(x+size/2,y+size);
ctx.lineTo(x,y+size/2);
ctx.closePath();
} else if(shape==='star'){
starPath(ctx, x+size/2, y+size/2, size*0.52, size*0.22, 5);
} else if(shape==='hexagon'){
hexagonPath(ctx, x+size/2, y+size/2, size*0.55);
} else if(shape==='rounded'){
variableRoundRect(ctx,x,y,size,size,size*0.3,size*0.3,size*0.3,size*0.3);
} else {
ctx.rect(x,y,size,size);
}
ctx.fill();
}
function makeGradient(ctx, colorMode, color1, color2, size, angleDeg){
if(colorMode === 'linear'){
const rad = (angleDeg||0) * Math.PI/180;
const cx = size/2, cy = size/2;
const dx = Math.cos(rad) * size/2;
const dy = Math.sin(rad) * size/2;
const g = ctx.createLinearGradient(cx-dx, cy-dy, cx+dx, cy+dy);
g.addColorStop(0,color1); g.addColorStop(1,color2);
return g;
} else if(colorMode === 'radial'){
const g = ctx.createRadialGradient(size/2,size/2,size*0.05,size/2,size/2,size*0.7);
g.addColorStop(0,color1); g.addColorStop(1,color2);
return g;
}
return color1;
}
/* ---------- logo drawing ---------- */
function drawLogo(ctx, cx, cy, r, shape, color, bgColor){
if(shape === 'none') return;
// knockout background circle
ctx.beginPath();
ctx.arc(cx,cy,r,0,Math.PI*2);
ctx.fillStyle = bgColor;
ctx.fill();
ctx.fillStyle = color;
const ir = r*0.62;
ctx.beginPath();
if(shape==='dot'){
ctx.arc(cx,cy,ir*0.7,0,Math.PI*2);
} else if(shape==='square'){
const s = ir*1.1;
variableRoundRect(ctx, cx-s/2, cy-s/2, s, s, s*0.2,s*0.2,s*0.2,s*0.2);
} else if(shape==='heart'){
const s = ir*1.3;
ctx.moveTo(cx, cy+s*0.35);
ctx.bezierCurveTo(cx+s*0.7, cy-s*0.3, cx+s*0.15, cy-s*0.65, cx, cy-s*0.2);
ctx.bezierCurveTo(cx-s*0.15, cy-s*0.65, cx-s*0.7, cy-s*0.3, cx, cy+s*0.35);
ctx.closePath();
} else if(shape==='bolt'){
const s = ir*1.4;
ctx.moveTo(cx+s*0.1, cy-s*0.6);
ctx.lineTo(cx-s*0.35, cy+s*0.05);
ctx.lineTo(cx-s*0.05, cy+s*0.05);
ctx.lineTo(cx-s*0.15, cy+s*0.6);
ctx.lineTo(cx+s*0.4, cy-s*0.1);
ctx.lineTo(cx+s*0.05, cy-s*0.1);
ctx.closePath();
} else if(shape==='wifi'){
ctx.fill(); // flush knockout first
ctx.beginPath();
ctx.arc(cx, cy+ir*0.35, ir*0.14, 0, Math.PI*2);
ctx.fill();
ctx.lineWidth = ir*0.16;
ctx.strokeStyle = color;
for(let i=1;i<=2;i++){
ctx.beginPath();
ctx.arc(cx, cy+ir*0.35, ir*0.4*i, Math.PI*1.25, Math.PI*1.75);
ctx.stroke();
}
return;
}
ctx.fill();
}
/* ---------- main renderer ---------- */
function renderQR(canvas, opts){
const {
text, ec='M', module=8, quiet=2,
bodyShape='square', eyeFrame='square', eyeBall='square',
colorMode='solid', color1='#000000', color2='#000000', gradAngle=45,
eyeColorMode=false, eyeColor1='#000000', eyeColor2='#000000',
bgColor='#ffffff', rotation=0,
frameStyle='none', labelText='SCAN MICH', labelColor='#111111',
logoShape='none', logoColor='#111111', logoBg='#ffffff'
} = opts;
const matrix = getMatrix(text, ec);
const n = matrix.length;
const qrSize = (n + quiet*2) * module;
// offscreen canvas: pure QR code (rotatable), no frame
const qrCanvas = document.createElement('canvas');
qrCanvas.width = qrSize;
qrCanvas.height = qrSize;
const qctx = qrCanvas.getContext('2d');
qctx.fillStyle = bgColor;
qctx.fillRect(0,0,qrSize,qrSize);
const bodyFill = makeGradient(qctx, colorMode, color1, color2, qrSize, gradAngle);
const eyeFill = eyeColorMode ? makeGradient(qctx, colorMode==='solid'?'solid':colorMode, eyeColor1, eyeColor2, qrSize, gradAngle) : bodyFill;
const offset = quiet*module;
qctx.fillStyle = bodyFill;
for(let r=0;r<n;r++){
for(let c=0;c<n;c++){
if(!matrix[r][c]) continue;
if(isInEye(r,c,n)) continue;
const x = offset + c*module;
const y = offset + r*module;
const neighbors = {
top: r>0 && matrix[r-1][c] && !isInEye(r-1,c,n),
bottom: r<n-1 && matrix[r+1][c] && !isInEye(r+1,c,n),
left: c>0 && matrix[r][c-1] && !isInEye(r,c-1,n),
right: c<n-1 && matrix[r][c+1] && !isInEye(r,c+1,n)
};
drawBodyModule(qctx, x, y, module, bodyShape, neighbors, r, c);
}
}
qctx.fillStyle = eyeFill;
const eyePositions = [[0,0],[0,n-7],[n-7,0]];
eyePositions.forEach(([er,ecol])=>{
const x = offset + ecol*module;
const y = offset + er*module;
const s = module*7;
drawEyeFrame(qctx,x,y,s,eyeFrame);
const ballSize = module*3;
const bx = x + module*2;
const by = y + module*2;
drawEyeBall(qctx,bx,by,ballSize,eyeBall);
});
// logo on top of QR (before rotation, so it rotates together)
if(logoShape !== 'none'){
const r = qrSize * 0.13;
drawLogo(qctx, qrSize/2, qrSize/2, r, logoShape, logoColor, logoBg);
}
/* ---- compose final canvas with frame/label + rotation ---- */
const hasLabel = frameStyle === 'label-bottom' || frameStyle === 'label-top';
const framePad = frameStyle === 'none' ? 0 : Math.round(qrSize*0.09);
const bandH = hasLabel ? Math.round(qrSize*0.16) : 0;
const totalW = qrSize + framePad*2;
const totalH = qrSize + framePad*2 + bandH;
canvas.width = totalW;
canvas.height = totalH;
const ctx = canvas.getContext('2d');
ctx.clearRect(0,0,totalW,totalH);
if(frameStyle !== 'none'){
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.18)';
ctx.shadowBlur = qrSize*0.03;
ctx.shadowOffsetY = qrSize*0.01;
ctx.fillStyle = '#ffffff';
variableRoundRect(ctx, 2,2, totalW-4, totalH-4, 18,18,18,18);
ctx.fill();
ctx.restore();
}
let qrX = framePad, qrY = framePad;
if(frameStyle === 'label-top') qrY = framePad + bandH;
// rotate only the QR portion around its own center
ctx.save();
const cx = qrX + qrSize/2, cy = qrY + qrSize/2;
ctx.translate(cx,cy);
ctx.rotate((rotation||0)*Math.PI/180);
ctx.drawImage(qrCanvas, -qrSize/2, -qrSize/2);
ctx.restore();
if(hasLabel){
const bandY = frameStyle === 'label-bottom' ? framePad + qrSize + (bandH-qrSize*0)*0 : 0;
ctx.fillStyle = labelColor;
ctx.font = `bold ${Math.round(bandH*0.5)}px -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const textY = frameStyle === 'label-bottom'
? framePad + qrSize + bandH/2
: framePad*0.55 + bandH*0.5;
// small arrow accent
ctx.fillText(labelText || 'SCAN MICH', totalW/2, textY);
}
}
/* ---------- gallery config ---------- */
const presets = [
{name:'Klassisch Schwarz', desc:'Eckig, kein Verlauf', bodyShape:'square', eyeFrame:'square', eyeBall:'square', colorMode:'solid', color1:'#111111', color2:'#111111', bgColor:'#ffffff'},
{name:'Soft Rounded', desc:'Abgerundete Module & Eyes', bodyShape:'rounded', eyeFrame:'rounded', eyeBall:'circle', colorMode:'solid', color1:'#1f2937', color2:'#1f2937', bgColor:'#ffffff'},
{name:'Dot Matrix', desc:'Punkte, runde Eyes', bodyShape:'dots', eyeFrame:'circle', eyeBall:'circle', colorMode:'solid', color1:'#0f172a', color2:'#0f172a', bgColor:'#ffffff'},
{name:'Violett-Türkis Fluss', desc:'Classy + linearer Verlauf', bodyShape:'classy', eyeFrame:'rounded', eyeBall:'circle', colorMode:'linear', color1:'#7c5cff', color2:'#00d4b5', gradAngle:45, bgColor:'#ffffff'},
{name:'Sunset Dots', desc:'Punkte + radialer Verlauf', bodyShape:'dots', eyeFrame:'leaf', eyeBall:'circle', colorMode:'radial', color1:'#ff5f6d', color2:'#ffc371', bgColor:'#fff8f0'},
{name:'Diamant Grid', desc:'Rautenförmige Module', bodyShape:'diamond', eyeFrame:'square', eyeBall:'diamond', colorMode:'solid', color1:'#111827', color2:'#111827', bgColor:'#ffffff'},
{name:'Classy Rounded Blue', desc:'Fließend, blauer Verlauf', bodyShape:'classy-rounded', eyeFrame:'rounded', eyeBall:'rounded', colorMode:'linear', color1:'#0ea5e9', color2:'#1e3a8a', gradAngle:90, bgColor:'#ffffff'},
{name:'Star Burst', desc:'Sterne als Module', bodyShape:'star', eyeFrame:'circle', eyeBall:'star', colorMode:'solid', color1:'#f59e0b', color2:'#f59e0b', bgColor:'#111111'},
{name:'Mint auf Dunkel', desc:'Dots, dunkler Hintergrund', bodyShape:'dots', eyeFrame:'rounded', eyeBall:'circle', colorMode:'solid', color1:'#00d4b5', color2:'#00d4b5', bgColor:'#0f1115'},
{name:'Rosa Verlauf Leaf-Eyes', desc:'Blatt-förmige Ecken', bodyShape:'rounded', eyeFrame:'leaf', eyeBall:'circle', colorMode:'linear', color1:'#f472b6', color2:'#a855f7', gradAngle:120, bgColor:'#fdf4ff'},
{name:'Feuer Diamant', desc:'Diamant + radialer Verlauf', bodyShape:'diamond', eyeFrame:'circle', eyeBall:'diamond', colorMode:'radial', color1:'#f97316', color2:'#7c2d12', bgColor:'#fff7ed'},
{name:'Monochrome Classy', desc:'Fließende Formen, Grautöne', bodyShape:'classy', eyeFrame:'square', eyeBall:'rounded', colorMode:'linear', color1:'#374151', color2:'#9ca3af', gradAngle:60, bgColor:'#ffffff'},
{name:'Ocean Dots', desc:'Punkte, Kreis-Eyes, Blauverlauf', bodyShape:'dots', eyeFrame:'circle', eyeBall:'circle', colorMode:'linear', color1:'#0369a1', color2:'#06b6d4', gradAngle:30, bgColor:'#f0f9ff'},
{name:'Neon auf Schwarz', desc:'Rounded, grelle Farben', bodyShape:'rounded', eyeFrame:'rounded', eyeBall:'circle', colorMode:'linear', color1:'#39ff14', color2:'#00e5ff', gradAngle:15, bgColor:'#000000'},
{name:'Klassisch mit Kreis-Eyes', desc:'Eckige Module, runde Ecken-Kerne', bodyShape:'square', eyeFrame:'square', eyeBall:'circle', colorMode:'solid', color1:'#1e293b', color2:'#1e293b', bgColor:'#ffffff'},
{name:'Gold Classy Rounded', desc:'Elegant, goldener Verlauf', bodyShape:'classy-rounded', eyeFrame:'leaf', eyeBall:'circle', colorMode:'linear', color1:'#b45309', color2:'#fde68a', gradAngle:200, bgColor:'#1c1917'},
{name:'Pastell Diamant', desc:'Diamant-Module, sanfte Farben', bodyShape:'diamond', eyeFrame:'rounded', eyeBall:'rounded', colorMode:'linear', color1:'#a78bfa', color2:'#f9a8d4', gradAngle:80, bgColor:'#fdf2ff'},
{name:'Sterne Regenbogen', desc:'Sterne + Multicolor-Verlauf', bodyShape:'star', eyeFrame:'rounded', eyeBall:'rounded', colorMode:'radial', color1:'#ef4444', color2:'#3b82f6', bgColor:'#ffffff'},
{name:'Minimal Dots Grau', desc:'Sehr dezent, für Print', bodyShape:'dots', eyeFrame:'square', eyeBall:'square', colorMode:'solid', color1:'#4b5563', color2:'#4b5563', bgColor:'#ffffff'},
{name:'Cyberpunk Classy', desc:'Fließend, Pink/Cyan auf Schwarz', bodyShape:'classy', eyeFrame:'circle', eyeBall:'diamond', colorMode:'linear', color1:'#ff00c8', color2:'#00fff7', gradAngle:135, bgColor:'#0a0a0a'},
{name:'Hexagon Grid', desc:'Sechseckige Module', bodyShape:'hexagon', eyeFrame:'hexagon', eyeBall:'hexagon', colorMode:'solid', color1:'#0f172a', color2:'#0f172a', bgColor:'#ffffff'},
{name:'Plus Muster', desc:'Kreuz-Module, Retro-Look', bodyShape:'plus', eyeFrame:'square', eyeBall:'square', colorMode:'solid', color1:'#dc2626', color2:'#dc2626', bgColor:'#fff1f2'},
{name:'Liquid Blob Lila', desc:'Fließende Blob-Module', bodyShape:'liquid', eyeFrame:'flower', eyeBall:'circle', colorMode:'linear', color1:'#8b5cf6', color2:'#ec4899', gradAngle:45, bgColor:'#ffffff'},
{name:'Mosaic Textur', desc:'Gemischte Formen pro Modul', bodyShape:'mosaic', eyeFrame:'rounded', eyeBall:'circle', colorMode:'solid', color1:'#1e293b', color2:'#1e293b', bgColor:'#f8fafc'},
{name:'Karte mit Schatten', desc:'Weiße Card, klassisch', bodyShape:'rounded', eyeFrame:'rounded', eyeBall:'circle', colorMode:'solid', color1:'#111111', color2:'#111111', bgColor:'#ffffff', frameStyle:'card'},
{name:'Scan-Mich Banner', desc:'Label unten, Call-to-Action', bodyShape:'dots', eyeFrame:'circle', eyeBall:'circle', colorMode:'linear', color1:'#7c5cff', color2:'#00d4b5', gradAngle:45, bgColor:'#ffffff', frameStyle:'label-bottom', labelText:'SCAN MICH', labelColor:'#111111'},
{name:'Logo Herz', desc:'Zentrales Herz-Logo', bodyShape:'rounded', eyeFrame:'rounded', eyeBall:'circle', colorMode:'linear', color1:'#f43f5e', color2:'#fb923c', gradAngle:45, bgColor:'#ffffff', logoShape:'heart', logoColor:'#f43f5e', logoBg:'#ffffff'},
{name:'WLAN-Logo Business', desc:'Wifi-Symbol, separate Eye-Farbe', bodyShape:'classy-rounded', eyeFrame:'rounded', eyeBall:'circle', colorMode:'solid', color1:'#1e3a8a', color2:'#1e3a8a', bgColor:'#ffffff', logoShape:'wifi', logoColor:'#1e3a8a', logoBg:'#ffffff', eyeColorMode:true, eyeColor1:'#f97316', eyeColor2:'#f97316'},
{name:'Blitz Neon', desc:'Bolt-Logo auf Neon-Verlauf', bodyShape:'dots', eyeFrame:'circle', eyeBall:'circle', colorMode:'linear', color1:'#facc15', color2:'#f97316', gradAngle:60, bgColor:'#111111', logoShape:'bolt', logoColor:'#111111', logoBg:'#facc15'},
{name:'Gedrehtes Muster', desc:'45° Rotation, Diamant-Module', bodyShape:'diamond', eyeFrame:'square', eyeBall:'diamond', colorMode:'solid', color1:'#0f172a', color2:'#0f172a', bgColor:'#ffffff', rotation:45},
{name:'Zweifarbige Eyes', desc:'Body schwarz, Eyes im Verlauf', bodyShape:'rounded', eyeFrame:'rounded', eyeBall:'circle', colorMode:'solid', color1:'#111111', color2:'#111111', bgColor:'#ffffff', eyeColorMode:true, eyeColor1:'#7c5cff', eyeColor2:'#00d4b5'},
{name:'Business Karte Top-Label', desc:'Label oben, dezentes Business-Design', bodyShape:'square', eyeFrame:'rounded', eyeBall:'square', colorMode:'solid', color1:'#111827', color2:'#111827', bgColor:'#ffffff', frameStyle:'label-top', labelText:'JETZT SCANNEN', labelColor:'#111827'}
];
/* ---------- wiring ---------- */
const dataInput = document.getElementById('dataInput');
const bodyShapeSel = document.getElementById('bodyShape');
const eyeFrameSel = document.getElementById('eyeFrame');
const eyeBallSel = document.getElementById('eyeBall');
const colorModeSel = document.getElementById('colorMode');
const color1Input = document.getElementById('color1');
const color2Input = document.getElementById('color2');
const gradAngleInput = document.getElementById('gradAngle');
const angleVal = document.getElementById('angleVal');
const angleField = document.getElementById('angleField');
const eyeColorToggle = document.getElementById('eyeColorToggle');
const eyeColorFields = document.getElementById('eyeColorFields');
const eyeColor1Input = document.getElementById('eyeColor1');
const eyeColor2Input = document.getElementById('eyeColor2');
const bgColorInput = document.getElementById('bgColor');
const ecLevelSel = document.getElementById('ecLevel');
const frameStyleSel = document.getElementById('frameStyle');
const labelFields = document.getElementById('labelFields');
const labelTextInput = document.getElementById('labelText');
const labelColorInput = document.getElementById('labelColor');
const logoShapeSel = document.getElementById('logoShape');
const logoColorInput = document.getElementById('logoColor');
const logoBgInput = document.getElementById('logoBg');
const moduleSizeInput = document.getElementById('moduleSize');
const moduleVal = document.getElementById('moduleVal');
const quietZoneInput = document.getElementById('quietZone');
const quietVal = document.getElementById('quietVal');
const rotationInput = document.getElementById('rotation');
const rotVal = document.getElementById('rotVal');
const liveCanvas = document.getElementById('liveCanvas');
const downloadBtn = document.getElementById('downloadBtn');
const randomBtn = document.getElementById('randomBtn');
const galleryCount = document.getElementById('galleryCount');
function currentOpts(){
return {
text: dataInput.value || 'https://qrmaster.com',
ec: ecLevelSel.value,
module: parseInt(moduleSizeInput.value,10),
quiet: parseInt(quietZoneInput.value,10),
bodyShape: bodyShapeSel.value,
eyeFrame: eyeFrameSel.value,
eyeBall: eyeBallSel.value,
colorMode: colorModeSel.value,
color1: color1Input.value,
color2: color2Input.value,
gradAngle: parseInt(gradAngleInput.value,10),
eyeColorMode: eyeColorToggle.checked,
eyeColor1: eyeColor1Input.value,
eyeColor2: eyeColor2Input.value,
bgColor: bgColorInput.value,
rotation: parseInt(rotationInput.value,10),
frameStyle: frameStyleSel.value,
labelText: labelTextInput.value,
labelColor: labelColorInput.value,
logoShape: logoShapeSel.value,
logoColor: logoColorInput.value,
logoBg: logoBgInput.value
};
}
function refreshLive(){
angleVal.textContent = gradAngleInput.value + '°';
moduleVal.textContent = moduleSizeInput.value;
quietVal.textContent = quietZoneInput.value;
rotVal.textContent = rotationInput.value + '°';
angleField.style.display = colorModeSel.value === 'linear' ? '' : 'none';
eyeColorFields.style.display = eyeColorToggle.checked ? 'grid' : 'none';
labelFields.style.display = (frameStyleSel.value === 'label-bottom' || frameStyleSel.value === 'label-top') ? 'grid' : 'none';
renderQR(liveCanvas, currentOpts());
}
[dataInput, bodyShapeSel, eyeFrameSel, eyeBallSel, colorModeSel, color1Input, color2Input,
gradAngleInput, eyeColorToggle, eyeColor1Input, eyeColor2Input, bgColorInput, ecLevelSel,
frameStyleSel, labelTextInput, labelColorInput, logoShapeSel, logoColorInput, logoBgInput,
moduleSizeInput, quietZoneInput, rotationInput]
.forEach(el => el.addEventListener('input', refreshLive));
downloadBtn.addEventListener('click', ()=>{
const link = document.createElement('a');
link.download = 'qr-code.png';
link.href = liveCanvas.toDataURL('image/png');
link.click();
});
function rand(arr){ return arr[Math.floor(Math.random()*arr.length)]; }
function randColor(){
const h = Math.floor(Math.random()*360);
const s = 60 + Math.floor(Math.random()*30);
const l = 35 + Math.floor(Math.random()*25);
return `hsl(${h} ${s}% ${l}%)`;
}
function hslToHex(hsl){
// fallback: use a canvas trick to normalize any CSS color to hex
const d = document.createElement('canvas').getContext('2d');
d.fillStyle = hsl;
return d.fillStyle;
}
randomBtn.addEventListener('click', ()=>{
const bodyShapes = Array.from(bodyShapeSel.options).map(o=>o.value);
const eyeFrames = Array.from(eyeFrameSel.options).map(o=>o.value);
const eyeBalls = Array.from(eyeBallSel.options).map(o=>o.value);
const colorModes = ['solid','linear','radial'];
const frameStyles = Array.from(frameStyleSel.options).map(o=>o.value);
const logoShapes = Array.from(logoShapeSel.options).map(o=>o.value);
bodyShapeSel.value = rand(bodyShapes);
eyeFrameSel.value = rand(eyeFrames);
eyeBallSel.value = rand(eyeBalls);
colorModeSel.value = rand(colorModes);
color1Input.value = hslToHex(randColor());
color2Input.value = hslToHex(randColor());
gradAngleInput.value = Math.floor(Math.random()*360);
eyeColorToggle.checked = Math.random() < 0.35;
eyeColor1Input.value = hslToHex(randColor());
eyeColor2Input.value = hslToHex(randColor());
frameStyleSel.value = rand(frameStyles);
logoShapeSel.value = Math.random() < 0.3 ? rand(logoShapes) : 'none';
moduleSizeInput.value = 6 + Math.floor(Math.random()*6);
rotationInput.value = Math.random() < 0.2 ? Math.floor(Math.random()*4)*90 : 0;
refreshLive();
});
function applyPresetToEditor(p){
bodyShapeSel.value = p.bodyShape;
eyeFrameSel.value = p.eyeFrame;
eyeBallSel.value = p.eyeBall;
colorModeSel.value = p.colorMode;
color1Input.value = p.color1;
color2Input.value = p.color2;
gradAngleInput.value = p.gradAngle || 45;
eyeColorToggle.checked = !!p.eyeColorMode;
eyeColor1Input.value = p.eyeColor1 || p.color1;
eyeColor2Input.value = p.eyeColor2 || p.color2;
bgColorInput.value = p.bgColor;
frameStyleSel.value = p.frameStyle || 'none';
labelTextInput.value = p.labelText || 'SCAN MICH';
labelColorInput.value = p.labelColor || '#111111';
logoShapeSel.value = p.logoShape || 'none';
logoColorInput.value = p.logoColor || '#111111';
logoBgInput.value = p.logoBg || '#ffffff';
rotationInput.value = p.rotation || 0;
refreshLive();
document.querySelector('.customizer').scrollIntoView({behavior:'smooth', block:'center'});
}
function buildGallery(){
const gallery = document.getElementById('gallery');
galleryCount.textContent = presets.length + ' Varianten — anklicken zum Übernehmen';
presets.forEach(p=>{
const card = document.createElement('div');
card.className = 'card';
const swatch = document.createElement('div');
swatch.className = 'swatch';
const canvas = document.createElement('canvas');
swatch.appendChild(canvas);
card.appendChild(swatch);
const name = document.createElement('div');
name.className = 'name';
name.textContent = p.name;
card.appendChild(name);
const desc = document.createElement('div');
desc.className = 'desc';
desc.textContent = p.desc;
card.appendChild(desc);
card.addEventListener('click', ()=> applyPresetToEditor(p));
gallery.appendChild(card);
renderQR(canvas, {
text: 'https://qrmaster.com',
ec: p.logoShape && p.logoShape !== 'none' ? 'H' : 'M',
module: 5,
quiet: 2,
bodyShape: p.bodyShape,
eyeFrame: p.eyeFrame,
eyeBall: p.eyeBall,
colorMode: p.colorMode,
color1: p.color1,
color2: p.color2,
gradAngle: p.gradAngle || 45,
eyeColorMode: !!p.eyeColorMode,
eyeColor1: p.eyeColor1 || p.color1,
eyeColor2: p.eyeColor2 || p.color2,
bgColor: p.bgColor,
rotation: p.rotation || 0,
frameStyle: p.frameStyle || 'none',
labelText: p.labelText || 'SCAN MICH',
labelColor: p.labelColor || '#111111',
logoShape: p.logoShape || 'none',
logoColor: p.logoColor || '#111111',
logoBg: p.logoBg || '#ffffff'
});
});
}
refreshLive();
buildGallery();
</script>
</body>
</html>