<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no”>
<title>Sanctum Temporal Relay: Multiversal Scan</title>
<link href=”https://fonts.googleapis.com/css2?family=Cinzel:wght@600;700&family=Nova+Mono&display=swap” rel=”stylesheet”>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #0b021a; /* Deep Indigo/Night Sky */
touch-action: none;
user-select: none;
-webkit-user-select: none;
font-family: ‘Nova Mono’, monospace; /* Ancient/Mystical Tech Font */
}
canvas {
display: block;
}
#init-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #000;
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
flex-direction: column;
transition: opacity 1.0s;
/* Starfield pattern for dimensional travel */
background-image: radial-gradient(circle, #1a0833 1px, transparent 1px), radial-gradient(circle, #1a0833 1px, transparent 1px);
background-size: 20px 20px;
background-position: 0 0, 10px 10px;
}
.strange-font {
font-family: ‘Cinzel’, serif;
text-transform: uppercase;
letter-spacing: 5px;
font-weight: 700;
color: #ffcc00; /* Eye of Agamotto Gold */
text-shadow: 0 0 15px rgba(255, 204, 0, 0.8);
margin-bottom: 20px;
}
#init-btn {
background: #0a011a; /* Dark background */
border: 4px solid #00ffff; /* Astral Blue Border */
padding: 20px 40px;
font-size: 30px;
color: #dddddd;
cursor: pointer;
box-shadow: 0 0 20px rgba(0, 255, 255, 0.7);
transition: background 0.3s, transform 0.1s;
border-radius: 8px; /* Slightly rounded, ancient stone look */
}
#init-btn:active {
background: #ffcc00;
color: #111;
transform: scale(0.98);
box-shadow: 0 0 5px #ffcc00;
}
.subtitle {
color: #ffcc00;
background-color: #0a011a;
padding: 8px 18px;
margin-top: 35px;
font-size: 20px;
letter-spacing: 2px;
border: 1px solid #ffcc00;
box-shadow: 0 0 5px #ffcc00;
}
/* Arcane Sigil Placeholder */
.sanctum-sigil {
width: 80px;
height: 80px;
background: radial-gradient(circle, rgba(0, 255, 255, 0.2), transparent 70%);
border: 5px solid #00ffff;
border-radius: 50%;
position: relative;
margin-bottom: 20px;
display: flex;
justify-content: center;
align-items: center;
font-size: 40px;
color: #00ffff;
box-shadow: 0 0 10px #00ffff;
}
</style>
</head>
<body>
<div id=”init-overlay”>
<div class=”sanctum-sigil strange-font”>∆</div>
<div class=”strange-font” style=”font-size: 40px;”>THE SANCTUM RELAY</div>
<div id=”init-btn” class=”strange-font”>INITIATE TEMPORAL SCAN</div>
<div class=”subtitle strange-font”>”We protect your reality.”</div>
</div>
<canvas id=”c”></canvas>
<script src=”https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js”></script>
<script>
const canvas = document.getElementById(‘c’);
const ctx = canvas.getContext(‘2d’);
// — Configuration —
let width, height;
let frame = 0;
let active = false;
// 4×4 Grid
const COLS = 4;
const ROWS = 4;
// Dr. Strange / Arcane Palette
const colors = {
panel: ‘#2a0a4c’, // Deep Purple / Mystical Stone
panelLight: ‘#441473’,
bezel: ‘#08001a’, // Near Black Void
black: ‘#0a011a’,
white: ‘#f0f0f0’, // Parchment White
// Hero Colors
agamottoGold: ‘#ffcc00’, // Primary Gold/Time Energy
astralBlue: ‘#00ffff’, // Astral Projection / Energy
alertRed: ‘#ff0033’, // Interdimensional Danger
scroll: ‘#e3e3c9’, // Ancient Scroll/Paper color
// Sigil Lights
lightOrange: ‘#ff9900’,
lightGreen: ‘#00cc00’,
lightCyan: ‘#00ffff’,
lightMagenta: ‘#ff00ff’,
labelBg: ‘#1c0333’ // Darker mystical purple
};
// Arcane themed component deck
const TILE_DECK = [
‘temporal_flux’, ‘astral_monitor’, ‘chaos_threshold’, ‘wong_comm’,
‘dimension_shifter’, ‘spell_matrix’, ‘rune_decipher’, ‘ward_integrity’,
‘forbidden_texts’, ‘kamar_taj_link’, ‘magical_residue’, ‘nexus_power’,
‘ancient_records’, ‘binding_protocol’, ‘mirror_dimension_map’, ‘incursion_warning’,
‘reality_stability’, ‘vibration_dampener’, ‘temporal_anchor’, ‘arcane_feedback’
];
// — Audio System (Subtle, Temporal Bleeps) —
const Audio = {
ctx: null,
masterGain: null,
init: function() {
try {
const AC = window.AudioContext || window.webkitAudioContext;
this.ctx = new AC();
// Reduced master volume significantly for less intrusive experience
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = 0.1; // Very low volume for mystical hum
this.masterGain.connect(this.ctx.destination);
this.startDimensionalHum();
} catch (e) {
console.error(“Audio initialization failed:”, e);
}
},
startDimensionalHum: function() {
if(!this.ctx) return;
// Low, constant, slightly wavering ambient drone
const carrier = this.ctx.createOscillator();
carrier.type = ‘sine’;
carrier.frequency.value = 80;
// Subtle LFO on pitch
const lfo = this.ctx.createOscillator();
lfo.type = ‘triangle’;
lfo.frequency.value = 0.15; // Very slow wave
const lfoGain = this.ctx.createGain();
lfoGain.gain.value = 10; // Modulate frequency by 10Hz
lfo.connect(lfoGain);
lfoGain.connect(carrier.frequency);
const gainHum = this.ctx.createGain();
gainHum.gain.setValueAtTime(0.1, this.ctx.currentTime); // Low initial gain
carrier.connect(gainHum);
gainHum.connect(this.masterGain);
carrier.start();
lfo.start();
},
// New: Temporal scan sound (a short, ascending, slightly detuned chord)
playTemporalScan: function() {
if(!this.ctx) return;
const t = this.ctx.currentTime;
const freq1 = 220; // A3
const freq2 = 277; // C#4
const freq3 = 330; // E4
const duration = 0.8;
// Ascending tones
[freq1, freq2, freq3].forEach((f, index) => {
const osc = this.ctx.createOscillator();
osc.type = ‘sawtooth’; // Slight buzz for mystical texture
osc.frequency.setValueAtTime(f, t + index * 0.05); // Staggered start
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0, t);
// Attack
gain.gain.linearRampToValueAtTime(0.2, t + index * 0.05 + 0.1);
// Release
gain.gain.exponentialRampToValueAtTime(0.0001, t + duration + 0.1);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(t);
osc.stop(t + duration + 0.1);
});
}
};
// Helper function to calculate a font size that fits within a given max width
function getFitFontSize(text, maxWidth, baseHeight, fontFamily) {
// Start with a large size relative to the available height for quick convergence
let fontSize = baseHeight * 0.15;
ctx.font = `${fontSize}px ${fontFamily}`;
let textWidth = ctx.measureText(text).width;
// Decrease font size until the text fits the width, or hits a minimum
while (textWidth > maxWidth * 0.98 && fontSize > 5) { // Use 98% of width for safety margin
fontSize -= 0.5;
ctx.font = `${fontSize}px ${fontFamily}`;
textWidth = ctx.measureText(text).width;
}
return fontSize;
}
// — Drawing Helpers —
function drawPanel(x, y, w, h) {
// Arcane Stone Console with etched corners
const r = 10; // Increased rounding for ancient feel
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w – r, y);
ctx.arcTo(x + w, y, x + w, y + r, r);
ctx.lineTo(x + w, y + h – r);
ctx.arcTo(x + w, y + h, x + w – r, y + h, r);
ctx.lineTo(x + r, y + h);
ctx.arcTo(x, y + h, x, y + h – r, r);
ctx.lineTo(x, y + r);
ctx.arcTo(x, y, x + r, y, r);
ctx.closePath();
// Gradient fill for depth (Purple Stone)
const grad = ctx.createLinearGradient(x, y, x, y+h);
grad.addColorStop(0, colors.panelLight);
grad.addColorStop(1, colors.panel);
ctx.fillStyle = grad;
ctx.fill();
// Gold Etched Edge
ctx.lineWidth = 2;
ctx.strokeStyle = colors.agamottoGold;
ctx.stroke();
// Arcane Power Light (Cyan/Astral)
ctx.fillStyle = colors.astralBlue;
ctx.shadowBlur = 5;
ctx.shadowColor = colors.astralBlue;
ctx.beginPath(); ctx.arc(x+8, y+8, 3, 0, Math.PI*2); ctx.fill();
ctx.shadowBlur = 0;
}
// — Tile System —
class Tile {
constructor(c, r, type) {
this.c = c;
this.r = r;
this.type = type;
this.active = false;
this.seed = Math.random();
this.opacity = 1.0;
this.isFadingOut = false;
this.isFadingIn = false;
this.fadeSpeed = 0.04;
}
draw(x, y, w, h) {
ctx.save();
ctx.globalAlpha = this.opacity;
drawPanel(x, y, w, h);
const m = 8; // Increased margin
const ix = x+m, iy = y+m, iw = w-m*2, ih = h-m*2;
// Area reserved for the tile content screen (80% of inner height)
const screenH = ih * 0.8;
const labelY = iy + ih – m/2; // Position label text above the bottom edge
let labelText = this.getLabelText(this.type);
// Draw label background strip (occupies the bottom 20% of inner height)
ctx.fillStyle = colors.labelBg;
ctx.fillRect(ix, iy + screenH, iw, ih – screenH);
// Calculate font size for the bottom label
const labelStripHeight = ih – screenH;
let labelFontSize = getFitFontSize(labelText, iw * 0.9, labelStripHeight, “‘Nova Mono’, monospace”);
// — Tile Content —
switch(this.type) {
case ‘temporal_flux’: this.drawTemporalFlux(ix, iy, iw, screenH); break;
case ‘astral_monitor’: this.drawAstralMonitor(ix, iy, iw, screenH); break;
case ‘chaos_threshold’: this.drawChaosThreshold(ix, iy, iw, screenH); break;
case ‘wong_comm’: this.drawWongComm(ix, iy, iw, screenH); break;
case ‘dimension_shifter’: this.drawDimensionShifter(ix, iy, iw, screenH); break;
case ‘spell_matrix’: this.drawSpellMatrix(ix, iy, iw, screenH); break;
case ‘rune_decipher’: this.drawRuneDecipher(ix, iy, iw, screenH); break;
case ‘ward_integrity’: this.drawWardIntegrity(ix, iy, iw, screenH); break;
case ‘forbidden_texts’: this.drawForbiddenTexts(ix, iy, iw, screenH, ih); break;
case ‘kamar_taj_link’: this.drawKamarTajLink(ix, iy, iw, screenH); break;
case ‘magical_residue’: this.drawMagicalResidue(ix, iy, iw, screenH); break;
case ‘nexus_power’: this.drawNexusPower(ix, iy, iw, screenH, ih); break;
case ‘ancient_records’: this.drawAncientRecords(ix, iy, iw, screenH, ih); break;
case ‘binding_protocol’: this.drawBindingProtocol(ix, iy, iw, screenH); break;
case ‘mirror_dimension_map’: this.drawMirrorDimensionMap(ix, iy, iw, screenH); break;
case ‘incursion_warning’: this.drawIncursionWarning(ix, iy, iw, screenH, ih); break;
case ‘reality_stability’: this.drawRealityStability(ix, iy, iw, screenH); break;
case ‘vibration_dampener’: this.drawVibrationDampener(ix, iy, iw, screenH); break;
case ‘temporal_anchor’: this.drawTemporalAnchor(ix, iy, iw, screenH); break;
case ‘arcane_feedback’: this.drawArcaneFeedback(ix, iy, iw, screenH, ih); break;
case ‘agamotto_center’: this.drawAgamottoCenter(ix, iy, iw, screenH); break;
}
// Draw Text over the label strip
ctx.font = `bold ${labelFontSize}px ‘Nova Mono’, monospace`;
ctx.fillStyle = colors.white;
ctx.textAlign = ‘center’;
ctx.textBaseline = ‘bottom’;
ctx.fillText(labelText, ix+iw/2, labelY);
ctx.restore();
}
getLabelText(type) {
switch(type) {
case ‘temporal_flux’: return “TEMPORAL FLUX”;
case ‘astral_monitor’: return “ASTRAL SEPARATION”;
case ‘chaos_threshold’: return “CHAOS MAGIC ALERT”;
case ‘wong_comm’: return “KAMAR-TAJ RELAY”;
case ‘dimension_shifter’: return “GATEWAY CONTROL”;
case ‘spell_matrix’: return “ACTIVE SPELL MATRIX”;
case ‘rune_decipher’: return “RUNE DECIPHER”;
case ‘ward_integrity’: return “WARD INTEGRITY”;
case ‘forbidden_texts’: return “VAULT ACCESS”;
case ‘kamar_taj_link’: return “ANCIENT ONE RECORDS”;
case ‘magical_residue’: return “POWER SIGNATURE”;
case ‘nexus_power’: return “DIMENSION ENERGY”;
case ‘ancient_records’: return “CIVIL WAR FILES”;
case ‘binding_protocol’: return “MANDATORY RESTRAINT”;
case ‘mirror_dimension_map’: return “MIRROR MAP”;
case ‘incursion_warning’: return “INCURSION THREAT”;
case ‘reality_stability’: return “BASE REALITY”;
case ‘vibration_dampener’: return “PORTAL STABILITY”;
case ‘temporal_anchor’: return “TIME ANCHOR”;
case ‘arcane_feedback’: return “SANCTUM READINGS”;
case ‘agamotto_center’: return “EYE OF AGAMOTTO”;
default: return “ARCANE RELAY SYSTEM”;
}
}
// — Component Drawings —
drawTemporalFlux(ix, iy, iw, ih) {
ctx.fillStyle = colors.black;
ctx.fillRect(ix, iy, iw, ih);
const cx = ix+iw/2, cy = iy+ih/2;
// Simple clock dial
ctx.strokeStyle = colors.agamottoGold;
ctx.lineWidth = 1;
const r = iw*0.35;
ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI*2); ctx.stroke();
// Clock hand for temporal distortion
const angle = (frame * 0.05) % (Math.PI * 2);
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(angle);
ctx.fillStyle = colors.astralBlue;
ctx.fillRect(0, -1, r, 2); // Hour hand (long)
ctx.rotate(Math.PI/2); // 90 degree offset for “minute” hand
ctx.fillStyle = colors.agamottoGold;
ctx.fillRect(0, -0.5, r*0.6, 1); // Minute hand (shorter)
ctx.restore();
}
drawAstralMonitor(ix, iy, iw, ih) {
ctx.fillStyle = colors.astralBlue;
ctx.fillRect(ix, iy, iw, ih);
// Separation wave trace
ctx.strokeStyle = colors.white;
ctx.lineWidth = 2;
ctx.beginPath();
for(let i=0; i<iw; i+=2) {
// Two detuned sine waves for a separation effect
const y1 = iy + ih*0.5 + Math.sin(i*0.2 + frame*0.1) * ih*0.1;
const y2 = iy + ih*0.5 + Math.sin(i*0.2 + frame*0.1 + 0.5) * ih*0.1;
if (i === 0) {
ctx.moveTo(ix+i, y1);
} else {
ctx.lineTo(ix+i, y1);
}
}
ctx.stroke();
ctx.strokeStyle = colors.agamottoGold;
ctx.beginPath();
for(let i=0; i<iw; i+=2) {
const y2 = iy + ih*0.5 + Math.sin(i*0.2 + frame*0.1 + 0.5) * ih*0.1;
if (i === 0) {
ctx.moveTo(ix+i, y2);
} else {
ctx.lineTo(ix+i, y2);
}
}
ctx.stroke();
// Status text – Sized dynamically
const statusText = “STATE: CONVERGED”;
const statusFontSize = getFitFontSize(statusText, iw * 0.9, ih * 0.2, “‘Nova Mono'”);
ctx.font = `bold ${statusFontSize}px ‘Nova Mono’`;
ctx.fillStyle = colors.white;
ctx.textAlign = ‘center’;
ctx.textBaseline = ‘middle’;
ctx.fillText(statusText, ix+iw/2, iy+ih*0.85);
}
drawChaosThreshold(ix, iy, iw, ih) {
ctx.fillStyle = colors.black;
ctx.fillRect(ix, iy, iw, ih);
ctx.strokeStyle = colors.alertRed;
ctx.lineWidth = 3;
ctx.shadowBlur = 8;
ctx.shadowColor = colors.alertRed;
// Simple sine wave for instability
ctx.beginPath();
for(let i=0; i<iw; i+=5) {
const y = iy + ih*0.5 + Math.cos(i*0.2 + frame*0.5) * (ih*0.3); // Erratic movement
if(i==0) ctx.moveTo(ix+i, y); else ctx.lineTo(ix+i, y);
}
ctx.stroke();
ctx.shadowBlur = 0;
}
drawWongComm(ix, iy, iw, ih) {
ctx.fillStyle = colors.scroll;
ctx.fillRect(ix, iy, iw, ih);
const cx = ix+iw/2, cy = iy+ih/2;
// Simple book icon (The librarian’s touch)
ctx.fillStyle = colors.panel;
ctx.fillRect(cx-iw*0.2, cy-ih*0.3, iw*0.4, ih*0.6);
ctx.fillStyle = colors.agamottoGold;
ctx.fillRect(cx-iw*0.2, cy-ih*0.3, 5, ih*0.6); // Spine
// Status indicator
ctx.fillStyle = colors.lightGreen;
ctx.beginPath(); ctx.arc(cx + iw * 0.3, cy – ih * 0.3, Math.min(5, iw*0.07), 0, Math.PI*2); ctx.fill();
// Text – Sized dynamically
const lineText = “WONG: ACTIVE”;
const lineFontSize = getFitFontSize(lineText, iw * 0.8, ih * 0.2, “‘Nova Mono'”);
ctx.font = `bold ${lineFontSize}px ‘Nova Mono’`;
ctx.fillStyle = ‘#000’;
ctx.textAlign = ‘center’;
ctx.textBaseline = ‘top’;
ctx.fillText(lineText, cx, cy + ih*0.3 + 5);
}
drawDimensionShifter(ix, iy, iw, ih) {
ctx.fillStyle = colors.black;
ctx.fillRect(ix, iy, iw, ih);
const cx = ix+iw/2, cy = iy+ih/2;
// Ring Gate (Mandala) effect
ctx.strokeStyle = colors.lightOrange;
ctx.lineWidth = 3;
const r1 = iw*0.3;
const r2 = iw*0.1;
ctx.beginPath();
ctx.arc(cx, cy, r1, 0, Math.PI*2);
ctx.arc(cx, cy, r2, 0, Math.PI*2);
ctx.stroke();
// Sparks rotating
ctx.fillStyle = colors.lightOrange;
const numSparks = 6;
for(let i=0; i<numSparks; i++) {
const angle = (frame * 0.2) + (i * Math.PI * 2 / numSparks);
const x = cx + Math.cos(angle) * (r1 + r2) / 2;
const y = cy + Math.sin(angle) * (r1 + r2) / 2;
ctx.beginPath(); ctx.arc(x, y, 2, 0, Math.PI*2); ctx.fill();
}
}
drawSpellMatrix(ix, iy, iw, ih) {
ctx.fillStyle = colors.black;
ctx.fillRect(ix, iy, iw, ih);
const cx = ix+iw/2, cy = iy+ih/2;
// Simple 3×3 grid for the matrix
const matrixSize = Math.min(iw, ih) * 0.7;
const startX = cx – matrixSize/2;
const startY = cy – matrixSize/2;
const cellSize = matrixSize / 3;
ctx.strokeStyle = colors.agamottoGold;
ctx.lineWidth = 1;
ctx.strokeRect(startX, startY, matrixSize, matrixSize);
// Matrix activation (Flashing cells)
for(let r=0; r<3; r++) {
for(let c=0; c<3; c++) {
if ((r*3+c + Math.floor(frame/10)) % 5 === 0) {
ctx.fillStyle = colors.astralBlue;
ctx.shadowBlur = 5;
ctx.shadowColor = colors.astralBlue;
ctx.fillRect(startX + c*cellSize, startY + r*cellSize, cellSize, cellSize);
ctx.shadowBlur = 0;
}
}
}
}
drawRuneDecipher(ix, iy, iw, ih) {
ctx.fillStyle = colors.scroll;
ctx.fillRect(ix, iy, iw, ih);
ctx.strokeStyle = ‘#000’;
ctx.lineWidth = 2;
// Draw a simple Elder Futhark ‘Fehu’ rune (F)
const runeSize = Math.min(iw, ih) * 0.4;
const cx = ix+iw/2, cy = iy+ih/2;
ctx.beginPath();
ctx.moveTo(cx, cy – runeSize/2);
ctx.lineTo(cx, cy + runeSize/2);
ctx.moveTo(cx, cy – runeSize/2);
ctx.lineTo(cx + runeSize/2, cy);
ctx.moveTo(cx, cy – runeSize/2);
ctx.lineTo(cx + runeSize/2, cy – runeSize*0.1);
ctx.stroke();
// Deciphering lines
ctx.strokeStyle = colors.alertRed;
ctx.lineWidth = 1;
if (frame % 20 < 10) {
ctx.beginPath();
ctx.moveTo(cx – runeSize, cy);
ctx.lineTo(cx + runeSize, cy);
ctx.stroke();
}
}
drawWardIntegrity(ix, iy, iw, ih) {
ctx.fillStyle = colors.black;
ctx.fillRect(ix, iy, iw, ih);
const cx = ix+iw/2, cy = iy+ih/2;
// Circular shield icon
ctx.strokeStyle = colors.agamottoGold;
ctx.lineWidth = 3;
const r = iw*0.35;
ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI*2); ctx.stroke();
ctx.beginPath(); ctx.arc(cx, cy, r*0.7, 0, Math.PI*2); ctx.stroke();
// Small gap/vulnerability indication
ctx.strokeStyle = colors.alertRed;
ctx.lineWidth = 5;
ctx.beginPath(); ctx.arc(cx, cy, r, Math.PI*0.1 + Math.sin(frame*0.05)*0.2, Math.PI*0.15 + Math.sin(frame*0.05)*0.2); ctx.stroke();
}
drawForbiddenTexts(ix, iy, iw, ih, totalH) {
// Book stack look
ctx.fillStyle = colors.scroll;
ctx.fillRect(ix, iy, iw, ih);
// Binding stripes
ctx.fillStyle = colors.panel;
ctx.fillRect(ix, iy + ih*0.2, iw, 5);
ctx.fillRect(ix, iy + ih*0.5, iw, 5);
ctx.fillRect(ix, iy + ih*0.8, iw, 5);
// Text – Sized dynamically
const titleText = “CRITICAL DATA”;
const titleFontSize = getFitFontSize(titleText, iw * 0.9, ih * 0.2, “‘Cinzel'”);
ctx.fillStyle = ‘#000’;
ctx.font = `bold ${titleFontSize}px ‘Cinzel’`;
ctx.textAlign = ‘center’;
ctx.textBaseline = ‘middle’;
ctx.fillText(titleText, ix+iw/2, iy+ih/2);
}
drawKamarTajLink(ix, iy, iw, ih) {
ctx.fillStyle = colors.black;
ctx.fillRect(ix, iy, iw, ih);
// Mountain outline (Himalayas reference)
ctx.strokeStyle = colors.lightGreen;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(ix, iy+ih);
ctx.lineTo(ix+iw*0.2, iy+ih*0.6);
ctx.lineTo(ix+iw*0.5, iy+ih*0.3);
ctx.lineTo(ix+iw*0.8, iy+ih*0.7);
ctx.lineTo(ix+iw, iy+ih*0.5);
ctx.stroke();
// Transmission pulse
ctx.fillStyle = colors.lightGreen;
const cx = ix + iw*0.5;
const cy = iy + ih*0.3;
const pulseR = Math.sin(frame*0.2) * 8 + 2;
ctx.beginPath(); ctx.arc(cx, cy, pulseR, 0, Math.PI*2); ctx.fill();
}
drawMagicalResidue(ix, iy, iw, ih) {
ctx.fillStyle = ‘#000’;
ctx.fillRect(ix, iy, iw, ih);
// Animated energy particles (Arcane dust)
ctx.fillStyle = colors.lightMagenta;
const particleCount = 20;
for(let i=0; i<particleCount; i++) {
const x = ix + (i*17 + frame*0.5 + this.seed*100) % iw;
const y = iy + (i*13 + frame*0.8 + this.seed*80) % ih;
ctx.beginPath(); ctx.arc(x, y, 1, 0, Math.PI*2); ctx.fill();
}
}
drawNexusPower(ix, iy, iw, ih, totalH) {
ctx.fillStyle = colors.black;
ctx.fillRect(ix, iy, iw, ih);
const cx = ix+iw/2, cy = iy+ih/2;
const tubeW = iw*0.2;
const tubeH = ih*0.7;
// Energy Container
ctx.strokeStyle = colors.white;
ctx.lineWidth = 3;
ctx.strokeRect(cx – tubeW/2, iy+ih*0.1, tubeW, tubeH);
// Energy fill (Gold)
const level = Math.sin(frame*0.05)*0.1 + 0.8; // High and fluctuating
const fillH = (tubeH – 6) * level;
ctx.fillStyle = colors.agamottoGold;
ctx.fillRect(cx – tubeW/2 + 3, iy+ih*0.1 + tubeH – fillH – 3, tubeW – 6, fillH);
// Label text – Sized dynamically
const percentText = `+${Math.round(level * 1000)} MJ`;
const percentFontSize = getFitFontSize(percentText, iw * 0.8, ih * 0.2, “‘Nova Mono'”);
ctx.fillStyle = colors.white;
ctx.font = `bold ${percentFontSize}px ‘Nova Mono’`;
ctx.textAlign = ‘center’;
ctx.textBaseline = ‘top’;
ctx.fillText(percentText, cx, iy + tubeH + ih*0.1 + 5);
}
drawAncientRecords(ix, iy, iw, ih, totalH) {
ctx.fillStyle = colors.scroll;
ctx.fillRect(ix, iy, iw, ih);
// Mock text glyphs (Kamar-Taj script)
ctx.fillStyle = ‘#000’;
const lineFontSize = Math.min(10, ih * 0.08);
const lineSpacing = lineFontSize * 1.5;
ctx.font = `${lineFontSize}px ‘Nova Mono’`;
for(let i=0; i<6; i++) {
const lineY = iy + 10 + i * lineSpacing;
const lineBlockH = lineFontSize * 0.8;
const charCount = Math.floor(iw / (lineFontSize * 1.5));
let mockLine = ”;
for(let j=0; j<charCount; j++) {
// Use random simple shapes/letters to resemble script
const chars = “∆∑∞∫∂√≈”;
mockLine += chars[Math.floor(Math.random()*chars.length)] + ‘ ‘;
}
ctx.fillText(mockLine, ix+10, lineY + lineBlockH);
}
}
drawBindingProtocol(ix, iy, iw, ih) {
ctx.fillStyle = colors.panel;
ctx.fillRect(ix, iy, iw, ih);
const cx = ix+iw/2;
// Mystical lock icon
ctx.strokeStyle = colors.agamottoGold;
ctx.lineWidth = 4;
const lockR = Math.min(20, iw*0.15);
ctx.beginPath(); ctx.arc(cx, iy+ih*0.4, lockR, Math.PI, Math.PI*2); ctx.stroke();
ctx.fillRect(cx – 5, iy+ih*0.4, 10, ih*0.3);
// Keyhole
ctx.fillStyle = colors.bezel;
ctx.beginPath(); ctx.arc(cx, iy+ih*0.6, 3, 0, Math.PI*2); ctx.fill();
// Blinking active state
if (frame % 30 < 15) {
ctx.fillStyle = colors.alertRed;
ctx.beginPath(); ctx.arc(cx + lockR + 5, iy+ih*0.4, 4, 0, Math.PI*2); ctx.fill();
}
}
drawMirrorDimensionMap(ix, iy, iw, ih) {
ctx.fillStyle = colors.black;
ctx.fillRect(ix, iy, iw, ih);
const cx = ix+iw/2, cy = iy+ih/2;
// Simple cube/box representation of space
ctx.strokeStyle = colors.astralBlue;
ctx.lineWidth = 2;
const size = iw*0.3;
// Front plane
ctx.strokeRect(cx-size/2, cy-size/2, size, size);
// Back plane (mirrored)
ctx.strokeRect(cx-size/4, cy-size/4, size, size);
// Connecting lines
ctx.beginPath();
ctx.moveTo(cx-size/2, cy-size/2); ctx.lineTo(cx-size/4, cy-size/4);
ctx.moveTo(cx+size/2, cy-size/2); ctx.lineTo(cx+size/4, cy-size/4);
ctx.stroke();
// Distortion effect (random lines)
ctx.strokeStyle = ‘rgba(255, 255, 255, 0.2)’;
for(let i=0; i<3; i++) {
ctx.beginPath();
ctx.moveTo(ix + Math.random()*iw, iy + Math.random()*ih);
ctx.lineTo(ix + Math.random()*iw, iy + Math.random()*ih);
ctx.stroke();
}
}
drawIncursionWarning(ix, iy, iw, ih, totalH) {
ctx.fillStyle = colors.alertRed;
ctx.fillRect(ix, iy, iw, ih);
// Symbol of reality collapse
ctx.strokeStyle = colors.white;
ctx.lineWidth = 5;
const cx = ix+iw/2, cy = iy+ih/2;
const crossSize = Math.min(iw, ih) * 0.3;
// X shape
ctx.beginPath();
ctx.moveTo(cx – crossSize, cy – crossSize); ctx.lineTo(cx + crossSize, cy + crossSize);
ctx.moveTo(cx + crossSize, cy – crossSize); ctx.lineTo(cx – crossSize, cy + crossSize);
ctx.stroke();
// Warning text – Sized dynamically
const warningText = “WARNING: REALITY BREACH”;
const warningFontSize = getFitFontSize(warningText, iw * 0.9, ih * 0.2, “‘Cinzel'”);
ctx.font = `bold ${warningFontSize}px ‘Cinzel’`;
ctx.fillStyle = colors.white;
ctx.textAlign = ‘center’;
ctx.textBaseline = ‘bottom’;
if (frame % 10 < 5) {
ctx.fillText(warningText, cx, iy+ih*0.85);
}
}
drawRealityStability(ix, iy, iw, ih) {
ctx.fillStyle = ‘#111’;
ctx.fillRect(ix, iy, iw, ih);
const barCount = 5;
const barSpacing = iw / (barCount + 1);
const barW = barSpacing * 0.8;
for(let i=0; i<barCount; i++) {
// Stability reading (mostly high, small fluctuations)
const h = (Math.sin(frame*0.1 + i*0.5)*0.1 + 0.9) * (ih*0.7);
const barX = ix + barSpacing + i * barSpacing;
ctx.fillStyle = colors.lightCyan;
ctx.fillRect(barX – barW/2, iy + ih – h – 5, barW, h);
}
}
drawVibrationDampener(ix, iy, iw, ih) {
ctx.fillStyle = colors.black;
ctx.fillRect(ix, iy, iw, ih);
const cx = ix+iw/2, cy = iy+ih/2;
// Portal opening effect (expanding circle)
ctx.strokeStyle = colors.lightOrange;
ctx.lineWidth = 3;
const r = iw*0.35;
const phase = frame % 100;
// Expanding ring
ctx.globalAlpha = 1 – (phase / 100);
ctx.beginPath(); ctx.arc(cx, cy, r * (phase / 100), 0, Math.PI*2); ctx.stroke();
ctx.globalAlpha = 1.0;
// Stable center point
ctx.fillStyle = colors.lightOrange;
ctx.beginPath(); ctx.arc(cx, cy, 3, 0, Math.PI*2); ctx.fill();
}
drawTemporalAnchor(ix, iy, iw, ih) {
ctx.fillStyle = colors.black;
ctx.fillRect(ix, iy, iw, ih);
const cx = ix+iw/2, cy = iy+ih/2;
// Arcane text rotating around center
const text = “∞∆∑∫”;
const r = iw*0.4;
const charSpacing = Math.PI * 2 / text.length;
ctx.font = `${Math.min(10, iw*0.1)}px ‘Nova Mono’`;
ctx.fillStyle = colors.agamottoGold;
ctx.textAlign = ‘center’;
ctx.textBaseline = ‘middle’;
for(let i=0; i<text.length; i++) {
const angle = frame*0.02 + i * charSpacing;
const x = cx + Math.cos(angle) * r;
const y = cy + Math.sin(angle) * r;
ctx.fillText(text[i], x, y);
}
}
drawArcaneFeedback(ix, iy, iw, ih, totalH) {
ctx.fillStyle = colors.black;
ctx.fillRect(ix, iy, iw, ih);
// Simple line graph for mystical energy reading
ctx.strokeStyle = colors.lightMagenta;
ctx.lineWidth = 2;
ctx.beginPath();
for(let i=0; i<iw; i+=5) {
const y = iy + ih*0.5 + Math.sin(i*0.1 + frame*0.1) * (ih*0.3);
if(i==0) ctx.moveTo(ix+i, y); else ctx.lineTo(ix+i, y);
}
ctx.stroke();
}
drawAgamottoCenter(ix, iy, iw, ih) {
// This tile is unused in the standard grid but serves as the title label
}
}
// — Grid System —
let grid = [];
let cellW, cellH, pad;
let temporalState = { active: false, type: ‘idle’, timer: 0 };
function initGrid() {
grid = [];
let availableTypes = […TILE_DECK];
for (let i = availableTypes.length – 1; i > 0; i–) {
const j = Math.floor(Math.random() * (i + 1));
[availableTypes[i], availableTypes[j]] = [availableTypes[j], availableTypes[i]];
}
let typeIndex = 0;
for(let r=0; r<ROWS; r++) {
for(let c=0; c<COLS; c++) {
let type;
// Center 2×2 area
if((r===1 || r===2) && (c===1 || c===2)) {
type = ‘agamotto_center’;
} else {
type = availableTypes[typeIndex];
typeIndex++;
}
grid.push(new Tile(c, r, type));
}
}
}
function resize() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
pad = 10;
cellW = (width – (pad * (COLS+1))) / COLS;
cellH = (height – (pad * (ROWS+1))) / ROWS;
}
function triggerTemporalScan() {
if(temporalState.active) return;
temporalState.active = true;
temporalState.timer = 0;
if(active) Audio.playTemporalScan();
// Scramble tiles purely for visual chaos
grid.forEach(t => {
if(t.type !== ‘agamotto_center’) {
t.seed = Math.random();
}
});
}
function drawMainScreen() {
// Calculate area of the center 2×2
const x1 = pad + 1*(cellW+pad);
const y1 = pad + 1*(cellH+pad);
const w = (cellW*2) + pad;
const h = (cellH*2) + pad;
drawPanel(x1, y1, w, h);
// Screen inset
const margin = 15;
const sx = x1+margin, sy = y1+margin, sw = w-margin*2, sh = h-margin*2;
ctx.fillStyle = colors.black;
ctx.fillRect(sx, sy, sw, sh);
// Screen Content
ctx.save();
ctx.beginPath(); ctx.rect(sx, sy, sw, sh); ctx.clip();
const cx = sx + sw/2;
const cy = sy + sh/2;
// Calculate font sizes relative to the main screen size (sw/sh)
const baseFontSize = sh * 0.05;
const statusFontSize = getFitFontSize(“REALITY 616”, sw/2, baseFontSize, “‘Nova Mono'”);
const scanTitleFontSize = getFitFontSize(“MULTIVERSAL SCANNING”, sw * 0.9, baseFontSize * 2, “‘Cinzel'”);
const scanStatusFontSize = getFitFontSize(“TEMPORAL SIGNATURE DETECTED”, sw * 0.9, baseFontSize * 2, “‘Cinzel'”);
if(!temporalState.active) {
// IDLE STATE: Multiverse Stability Monitor
// Draw rotating Eye of Agamotto symbol (simple mandala)
ctx.strokeStyle = colors.agamottoGold;
ctx.lineWidth = 3;
ctx.shadowBlur = 10;
ctx.shadowColor = colors.agamottoGold;
// Center Circle (The Eye)
ctx.beginPath();
ctx.arc(cx, cy, sh*0.1, 0, Math.PI*2);
ctx.stroke();
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(frame * 0.005); // Slow rotation
// Outer Ring 1
const r1 = sh*0.35;
ctx.beginPath();
ctx.arc(0, 0, r1, 0, Math.PI*2);
ctx.stroke();
// 8 spokes
const numSpokes = 8;
for(let i=0; i<numSpokes; i++) {
const angle = i * Math.PI * 2 / numSpokes;
ctx.rotate(angle);
ctx.beginPath();
ctx.moveTo(sh*0.1, 0);
ctx.lineTo(r1, 0);
ctx.stroke();
ctx.rotate(-angle);
}
ctx.restore();
ctx.shadowBlur = 0;
// Text Readout – Sized dynamically
ctx.font = `bold ${statusFontSize}px ‘Nova Mono’`;
ctx.fillStyle = colors.white;
ctx.textAlign = ‘left’;
ctx.textBaseline = ‘top’;
ctx.fillText(“REALITY 616”, sx+10, sy+10);
ctx.fillStyle = colors.astralBlue;
ctx.textAlign = ‘right’;
ctx.fillText(“THRESHOLD: NOMINAL”, sx+sw-10, sy+10);
} else {
// COMPUTING STATE – TEMPORAL SCAN
temporalState.timer++;
// Background color (Astral Blue for scanning)
ctx.fillStyle = colors.astralBlue;
ctx.fillRect(sx, sy, sw, sh);
// Swirling portal effect
ctx.strokeStyle = colors.agamottoGold;
ctx.lineWidth = 5;
const maxR = sh * 0.5;
const numRings = 5;
for(let i=0; i<numRings; i++) {
const r = maxR * (i+1) / numRings;
const angleOffset = frame * 0.1 + i * 0.5;
ctx.globalAlpha = 0.5 – i * 0.1;
ctx.beginPath();
ctx.arc(cx, cy, r, angleOffset, angleOffset + Math.PI * 1.8);
ctx.stroke();
}
ctx.globalAlpha = 1.0;
// Status Text
ctx.fillStyle = colors.white;
ctx.font = `bold ${scanTitleFontSize}px ‘Cinzel’`;
ctx.textAlign = ‘center’;
// Text Glow Effect
ctx.shadowBlur = 10;
ctx.shadowColor = colors.white;
ctx.textBaseline = ‘top’;
ctx.fillText(“MULTIVERSAL SCANNING”, cx, sy + 10);
// Data Readout (Flashing data)
ctx.font = `bold ${scanStatusFontSize}px ‘Cinzel’`;
ctx.textBaseline = ‘bottom’;
if (temporalState.timer % 10 < 5) {
ctx.fillText(“TEMPORAL SIGNATURE DETECTED”, cx, sy + sh – 10);
} else {
ctx.fillText(“CALIBRATING TIMESTREAM”, cx, sy + sh – 10);
}
ctx.shadowBlur = 0;
if (temporalState.timer > 150) {
temporalState.active = false; // Reset after a short period
}
}
ctx.restore();
// Panel edge glare
ctx.fillStyle = ‘rgba(255,204,0,0.05)’;
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(sx+sw, sy);
ctx.lineTo(sx, sy+sh);
ctx.fill();
}
function drawActionOverlay() {
if (temporalState.active && temporalState.timer < 30) {
const cx = width/2;
const cy = height/2;
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(Math.sin(temporalState.timer*0.2)*0.05); // Subtle dimensional instability shake
// Arcane Sigil Effect
ctx.strokeStyle = colors.alertRed;
ctx.lineWidth = 8;
const r = Math.min(width, height) * 0.2; // Responsive radius
// Pulsating, rotating triangle/mandala
const numPoints = 3;
ctx.beginPath();
for(let i=0; i<numPoints; i++) {
const angle = (frame * 0.2) + (i * Math.PI * 2 / numPoints);
const x = Math.cos(angle) * r;
const y = Math.sin(angle) * r;
if(i==0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.closePath();
ctx.stroke();
const overlayFontSize = getFitFontSize(“SCANNING”, r * 2, r * 0.5, “‘Cinzel'”);
ctx.font = `bold ${overlayFontSize}px ‘Cinzel’`;
ctx.textAlign = ‘center’;
ctx.textBaseline = ‘middle’;
// Text Glow Effect
ctx.shadowBlur = 15;
ctx.shadowColor = colors.alertRed;
ctx.fillStyle = colors.white;
ctx.strokeText(“SCANNING”, 0, 0);
ctx.fillText(“SCANNING”, 0, 0);
ctx.restore();
ctx.shadowBlur = 0;
}
}
function loop() {
frame++;
ctx.fillStyle = colors.black;
ctx.fillRect(0,0,width,height);
// Tile Refresh (Random arcane flicker)
if (frame % 40 === 0 && Math.random() < 0.1) {
const candidates = grid.filter(t => t.type !== ‘agamotto_center’ && !t.isFadingOut && !t.isFadingIn);
if (candidates.length > 0) {
candidates[Math.floor(Math.random() * candidates.length)].isFadingOut = true;
}
}
grid.forEach(t => {
const x = pad + t.c*(cellW+pad);
const y = pad + t.r*(cellH+pad);
if (t.isFadingOut) {
t.opacity = Math.max(0, t.opacity – t.fadeSpeed);
if (t.opacity === 0) {
t.isFadingOut = false;
// Strict Uniqueness Check
const currentTypes = grid.map(g => g.type).filter(g => g !== t.type && g !== ‘agamotto_center’);
const availableDeck = TILE_DECK.filter(tType => !currentTypes.includes(tType));
if (t.type !== ‘agamotto_center’ && availableDeck.length > 0) {
let newType = availableDeck[Math.floor(Math.random() * availableDeck.length)];
t.type = newType;
t.seed = Math.random();
t.isFadingIn = true;
} else if (t.type !== ‘agamotto_center’) {
t.type = TILE_DECK[Math.floor(Math.random() * TILE_DECK.length)];
t.seed = Math.random();
t.isFadingIn = true;
} else {
t.isFadingIn = true;
}
}
} else if (t.isFadingIn) {
t.opacity = Math.min(1, t.opacity + t.fadeSpeed);
if (t.opacity === 1) t.isFadingIn = false;
}
if(t.type === ‘agamotto_center’) return;
t.draw(x, y, cellW, cellH);
});
drawMainScreen();
drawActionOverlay();
requestAnimationFrame(loop);
}
// Inputs
const btn = document.getElementById(‘init-btn’);
btn.addEventListener(‘click’, () => {
active = true;
Audio.init();
btn.innerText = “BY THE VISHANTI!”;
document.querySelector(‘.subtitle’).innerText = “PROTECTING THE 616 REALITY”;
document.getElementById(‘init-overlay’).style.opacity = 0;
setTimeout(() => {
document.getElementById(‘init-overlay’).style.display = ‘none’;
triggerTemporalScan();
}, 1000);
});
// Use the new function name for interaction
canvas.addEventListener(‘mousedown’, () => triggerTemporalScan());
canvas.addEventListener(‘touchstart’, (e) => { e.preventDefault(); triggerTemporalScan(); }, {passive: false});
window.addEventListener(‘resize’, resize);
resize();
initGrid();
loop();
</script>
</body>
</html>
