> For the complete documentation index, see [llms.txt](https://wiki.playgama.com/playgama/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wiki.playgama.com/playgama/guides/monetization/intrinsic-ads/javascript.md).

# JavaScript

{% file src="/files/DbclEB28aoqinqcbZ3zz" %}

## Guide

Complete guide for integrating intrinsic in-game ads with vanilla JavaScript, without a framework or game engine.

### Table of Contents

* Quick Start
* Basic Integration
* 2D Canvas Example
* WebGL Example
* Multiple Billboards
* Advanced Features
* Complete Examples

***

### Quick Start

#### Minimal HTML Setup

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Anzu Ads - Vanilla JS</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            overflow: hidden;
            font-family: Arial, sans-serif;
        }
        #game-canvas {
            display: block;
            background: #000;
        }
    </style>
</head>
<body>
    <canvas id="game-canvas"></canvas>

    <!-- Anzu Web SDK -->
    <script src="intrinsic-ads-js-sdk/core/SDKLoader.js"></script>
    <script src="intrinsic-ads-js-sdk/utils/AnzuDebug.js"></script>
    <script src="intrinsic-ads-js-sdk/utils/AnzuMath.js"></script>
    <script src="intrinsic-ads-js-sdk/utils/UriSchema.js"></script>
    <script src="intrinsic-ads-js-sdk/core/AnzuCore.js"></script>
    <script src="intrinsic-ads-js-sdk/core/ChannelManager.js"></script>
    <script src="intrinsic-ads-js-sdk/core/RenderManager.js"></script>
    <script src="intrinsic-ads-js-sdk/AnzuAd.js"></script>

    <!-- Your game code -->
    <script src="game.js"></script>
</body>
</html>
```

***

### Basic Integration

#### 1. Initialize Anzu SDK

```javascript
// game.js

// Initialize Anzu (call once at startup)
AnzuCore.initialize({
    appKey: "YOUR_ANZU_APP_KEY",        // Get from Anzu dashboard
    logLevel: 1,                         // 0=Debug, 1=Info, 2=Warn, 3=Error, 4=None
    gdprConsentStatus: -1,               // -1=unknown, 0=no consent, 1=consent
    gdprConsentString: "",               // Optional GDPR consent string
    isCoppaRegulated: false              // Set to true if COPPA applies
});

console.log("Anzu SDK initialized!");
```

#### 2. Create an Ad Billboard

```javascript
// Create an ad instance
const billboard = new AnzuAd({
    channelName: "billboard_01",         // Unique identifier
    aspectRatio: 16 / 9,                 // Width / Height ratio
    isDynamic: true,                     // Allow dynamic ad switching
    isClickable: true,                   // Enable click interactions
    allowVideo: true,                    // Allow video ads
    allowAudio: false,                   // Mute videos (better for autoplay)
    shrinkToFit: true                    // Adjust to maintain aspect ratio
});

// Initialize the billboard
billboard.initialize();

console.log("Billboard created:", billboard.getChannelName());
```

#### 3. Main Game Loop

```javascript
let lastTime = performance.now();

function gameLoop(currentTime) {
    // Calculate delta time in seconds
    const deltaTime = (currentTime - lastTime) / 1000;
    lastTime = currentTime;

    // Update Anzu SDK (required every frame)
    AnzuCore.update(deltaTime);

    // Get the ad canvas
    const adCanvas = billboard.getCanvas();

    if (adCanvas) {
        // Use the canvas in your rendering
        // (see examples below)
    }

    // Report visibility to Anzu
    billboard.updateVisibility({
        angle: 0,           // Viewing angle (0-90 degrees)
        visibility: 1.0,    // Screen coverage (0-1)
        viewability: 1.0    // Viewability score (0-1)
    });

    // Continue loop
    requestAnimationFrame(gameLoop);
}

// Start the game loop
requestAnimationFrame(gameLoop);
```

***

### 2D Canvas Example

#### Complete 2D Billboard Implementation

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Anzu 2D Canvas Example</title>
    <style>
        body { margin: 0; background: #222; }
        canvas { display: block; margin: 20px auto; border: 2px solid #444; }
    </style>
</head>
<body>
    <canvas id="game-canvas"></canvas>

    <!-- Include Anzu SDK files -->
    <script src="intrinsic-ads-js-sdk/core/SDKLoader.js"></script>
    <script src="intrinsic-ads-js-sdk/utils/AnzuDebug.js"></script>
    <script src="intrinsic-ads-js-sdk/utils/AnzuMath.js"></script>
    <script src="intrinsic-ads-js-sdk/utils/UriSchema.js"></script>
    <script src="intrinsic-ads-js-sdk/core/AnzuCore.js"></script>
    <script src="intrinsic-ads-js-sdk/core/ChannelManager.js"></script>
    <script src="intrinsic-ads-js-sdk/core/RenderManager.js"></script>
    <script src="intrinsic-ads-js-sdk/AnzuAd.js"></script>

    <script>
        // Setup canvas
        const canvas = document.getElementById('game-canvas');
        const ctx = canvas.getContext('2d');
        canvas.width = 800;
        canvas.height = 600;

        // Initialize Anzu
        AnzuCore.initialize({
            appKey: "YOUR_ANZU_APP_KEY",
            logLevel: 1
        });

        // Create billboard
        const billboard = {
            ad: new AnzuAd({
                channelName: "billboard_2d",
                aspectRatio: 16 / 9,
                isDynamic: true,
                isClickable: true
            }),
            x: 200,
            y: 150,
            width: 400,
            height: 225 // 400 / (16/9)
        };

        billboard.ad.initialize();

        // Handle clicks
        canvas.addEventListener('click', (event) => {
            const rect = canvas.getBoundingClientRect();
            const x = event.clientX - rect.left;
            const y = event.clientY - rect.top;

            // Check if click is within billboard bounds
            if (x >= billboard.x && x <= billboard.x + billboard.width &&
                y >= billboard.y && y <= billboard.y + billboard.height) {
                billboard.ad.interact();
                console.log("Billboard clicked!");
            }
        });

        // Game loop
        let lastTime = performance.now();

        function gameLoop(currentTime) {
            const deltaTime = (currentTime - lastTime) / 1000;
            lastTime = currentTime;

            // Update Anzu
            AnzuCore.update(deltaTime);

            // Clear canvas
            ctx.fillStyle = '#1a1a1a';
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            // Draw billboard
            const adCanvas = billboard.ad.getCanvas();
            if (adCanvas && adCanvas.width > 0) {
                ctx.drawImage(
                    adCanvas,
                    billboard.x,
                    billboard.y,
                    billboard.width,
                    billboard.height
                );

                // Draw border
                ctx.strokeStyle = '#00ff00';
                ctx.lineWidth = 2;
                ctx.strokeRect(billboard.x, billboard.y, billboard.width, billboard.height);
            } else {
                // Placeholder while loading
                ctx.fillStyle = '#333';
                ctx.fillRect(billboard.x, billboard.y, billboard.width, billboard.height);
                ctx.fillStyle = '#fff';
                ctx.font = '20px Arial';
                ctx.textAlign = 'center';
                ctx.fillText('Loading Ad...', billboard.x + billboard.width/2, billboard.y + billboard.height/2);
            }

            // Calculate visibility
            const billboardBounds = {
                left: billboard.x,
                right: billboard.x + billboard.width,
                top: billboard.y,
                bottom: billboard.y + billboard.height
            };

            const canvasBounds = {
                left: 0,
                right: canvas.width,
                top: 0,
                bottom: canvas.height
            };

            // Check if billboard is visible
            const isVisible = !(
                billboardBounds.right < canvasBounds.left ||
                billboardBounds.left > canvasBounds.right ||
                billboardBounds.bottom < canvasBounds.top ||
                billboardBounds.top > canvasBounds.bottom
            );

            // Calculate screen coverage (simple approximation)
            const billboardArea = billboard.width * billboard.height;
            const canvasArea = canvas.width * canvas.height;
            const visibility = isVisible ? Math.min(1, billboardArea / canvasArea) : 0;

            // Report to Anzu
            billboard.ad.updateVisibility({
                angle: 0,              // 2D, always facing camera
                visibility: visibility,
                viewability: isVisible ? 1 : 0
            });

            requestAnimationFrame(gameLoop);
        }

        requestAnimationFrame(gameLoop);
    </script>
</body>
</html>
```

***

### WebGL Example

#### Complete WebGL Billboard Implementation

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Anzu WebGL Example</title>
    <style>
        body { margin: 0; background: #000; }
        canvas { display: block; }
    </style>
</head>
<body>
    <canvas id="webgl-canvas"></canvas>

    <!-- Include Anzu SDK files -->
    <script src="intrinsic-ads-js-sdk/core/SDKLoader.js"></script>
    <script src="intrinsic-ads-js-sdk/utils/AnzuDebug.js"></script>
    <script src="intrinsic-ads-js-sdk/utils/AnzuMath.js"></script>
    <script src="intrinsic-ads-js-sdk/utils/UriSchema.js"></script>
    <script src="intrinsic-ads-js-sdk/core/AnzuCore.js"></script>
    <script src="intrinsic-ads-js-sdk/core/ChannelManager.js"></script>
    <script src="intrinsic-ads-js-sdk/core/RenderManager.js"></script>
    <script src="intrinsic-ads-js-sdk/AnzuAd.js"></script>

    <script>
        // WebGL setup
        const canvas = document.getElementById('webgl-canvas');
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;

        const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');

        if (!gl) {
            alert('WebGL not supported!');
        }

        // Vertex shader
        const vertexShaderSource = `
            attribute vec2 a_position;
            attribute vec2 a_texCoord;
            varying vec2 v_texCoord;

            void main() {
                gl_Position = vec4(a_position, 0.0, 1.0);
                v_texCoord = a_texCoord;
            }
        `;

        // Fragment shader
        const fragmentShaderSource = `
            precision mediump float;
            uniform sampler2D u_texture;
            varying vec2 v_texCoord;

            void main() {
                gl_FragColor = texture2D(u_texture, v_texCoord);
            }
        `;

        // Compile shader
        function compileShader(source, type) {
            const shader = gl.createShader(type);
            gl.shaderSource(shader, source);
            gl.compileShader(shader);

            if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
                console.error('Shader compile error:', gl.getShaderInfoLog(shader));
                gl.deleteShader(shader);
                return null;
            }

            return shader;
        }

        // Create program
        const vertexShader = compileShader(vertexShaderSource, gl.VERTEX_SHADER);
        const fragmentShader = compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER);

        const program = gl.createProgram();
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);
        gl.linkProgram(program);

        if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
            console.error('Program link error:', gl.getProgramInfoLog(program));
        }

        gl.useProgram(program);

        // Create billboard geometry (quad)
        const positions = new Float32Array([
            -0.5,  0.28,  // Top left
             0.5,  0.28,  // Top right
            -0.5, -0.28,  // Bottom left
             0.5, -0.28   // Bottom right
        ]);

        const texCoords = new Float32Array([
            0, 0,  // Top left
            1, 0,  // Top right
            0, 1,  // Bottom left
            1, 1   // Bottom right
        ]);

        // Position buffer
        const positionBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);

        const positionLocation = gl.getAttribLocation(program, 'a_position');
        gl.enableVertexAttribArray(positionLocation);
        gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

        // Texture coordinate buffer
        const texCoordBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW);

        const texCoordLocation = gl.getAttribLocation(program, 'a_texCoord');
        gl.enableVertexAttribArray(texCoordLocation);
        gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);

        // Create texture
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

        // Initialize Anzu
        AnzuCore.initialize({
            appKey: "YOUR_ANZU_APP_KEY",
            logLevel: 1
        });

        // Create billboard
        const billboard = new AnzuAd({
            channelName: "webgl_billboard",
            aspectRatio: 16 / 9,
            isDynamic: true,
            isClickable: true
        });

        billboard.initialize();

        // Handle clicks
        canvas.addEventListener('click', () => {
            billboard.interact();
        });

        // Game loop
        let lastTime = performance.now();

        function render(currentTime) {
            const deltaTime = (currentTime - lastTime) / 1000;
            lastTime = currentTime;

            // Update Anzu
            AnzuCore.update(deltaTime);

            // Get ad canvas and update texture
            const adCanvas = billboard.getCanvas();
            if (adCanvas && adCanvas.width > 0) {
                gl.bindTexture(gl.TEXTURE_2D, texture);
                gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, adCanvas);
            }

            // Clear and render
            gl.clearColor(0.1, 0.1, 0.1, 1.0);
            gl.clear(gl.COLOR_BUFFER_BIT);
            gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

            // Report visibility
            billboard.updateVisibility({
                angle: 0,
                visibility: 1.0,
                viewability: 1.0
            });

            requestAnimationFrame(render);
        }

        requestAnimationFrame(render);
    </script>
</body>
</html>
```

***

### Multiple Billboards

#### Managing Multiple Ad Placements

```javascript
// Billboard manager
class BillboardManager {
    constructor() {
        this.billboards = [];
    }

    createBillboard(config) {
        const billboard = {
            ad: new AnzuAd({
                channelName: config.name,
                aspectRatio: config.aspectRatio || 16/9,
                isDynamic: true,
                isClickable: true,
                allowVideo: true,
                allowAudio: false
            }),
            x: config.x || 0,
            y: config.y || 0,
            width: config.width || 400,
            height: config.height || 225,
            visible: true
        };

        billboard.ad.initialize();
        this.billboards.push(billboard);

        return billboard;
    }

    update(deltaTime) {
        // Update all billboards
        this.billboards.forEach(billboard => {
            // Calculate visibility for each billboard
            const visibility = this.calculateVisibility(billboard);

            billboard.ad.updateVisibility({
                angle: 0,
                visibility: visibility,
                viewability: visibility > 0 ? 1 : 0
            });
        });
    }

    render(ctx) {
        this.billboards.forEach(billboard => {
            if (!billboard.visible) return;

            const adCanvas = billboard.ad.getCanvas();
            if (adCanvas && adCanvas.width > 0) {
                ctx.drawImage(
                    adCanvas,
                    billboard.x,
                    billboard.y,
                    billboard.width,
                    billboard.height
                );
            }
        });
    }

    calculateVisibility(billboard) {
        // Simple screen coverage calculation
        const screenArea = window.innerWidth * window.innerHeight;
        const billboardArea = billboard.width * billboard.height;
        return Math.min(1, billboardArea / screenArea);
    }

    handleClick(x, y) {
        // Check click against all billboards
        for (const billboard of this.billboards) {
            if (x >= billboard.x && x <= billboard.x + billboard.width &&
                y >= billboard.y && y <= billboard.y + billboard.height) {
                billboard.ad.interact();
                return true;
            }
        }
        return false;
    }

    destroy() {
        this.billboards.forEach(b => b.ad.destroy());
        this.billboards = [];
    }
}

// Usage
const manager = new BillboardManager();

manager.createBillboard({
    name: "top_banner",
    x: 100,
    y: 50,
    width: 600,
    height: 100,
    aspectRatio: 6
});

manager.createBillboard({
    name: "sidebar",
    x: 50,
    y: 200,
    width: 200,
    height: 200,
    aspectRatio: 1
});

// In game loop
function gameLoop(deltaTime) {
    AnzuCore.update(deltaTime);
    manager.update(deltaTime);
    manager.render(ctx);
}
```

***

### Advanced Features

#### 1. GDPR Consent Management

```javascript
// Check if user is in GDPR region
function isGDPRRegion() {
    // Implement your own logic or use a service
    return true; // Example
}

// Show consent dialog
function showConsentDialog() {
    return new Promise((resolve) => {
        // Your consent UI logic
        const hasConsent = confirm("Do you consent to personalized ads?");
        resolve(hasConsent ? 1 : 0);
    });
}

// Initialize with GDPR
async function initializeWithGDPR() {
    let consentStatus = -1; // Unknown

    if (isGDPRRegion()) {
        consentStatus = await showConsentDialog();
    }

    AnzuCore.initialize({
        appKey: "YOUR_ANZU_APP_KEY",
        logLevel: 1,
        gdprConsentStatus: consentStatus,
        gdprConsentString: "", // Optional CMP consent string
        isCoppaRegulated: false
    });
}

initializeWithGDPR();
```

#### 2. URI Schema Callbacks

```javascript
// Register custom URI scheme for deep linking
UriSchema.registerHook("myapp", (data) => {
    console.log("URI callback triggered!");
    console.log("Base URI:", data.baseUri);
    console.log("Arguments:", data.arguments);

    // Handle the callback
    if (data.arguments.action === "product") {
        const productId = data.arguments.id;
        showProduct(productId);
    }
});

function showProduct(productId) {
    console.log("Showing product:", productId);
    // Your product display logic
}
```

#### 3. Ad Statistics and Monitoring

```javascript
// Track ad performance
class AdMonitor {
    constructor(anzuAd) {
        this.ad = anzuAd;
        this.startTime = Date.now();
    }

    getStats() {
        const stats = this.ad.getChannelStats();
        const uptime = (Date.now() - this.startTime) / 1000;

        return {
            channelName: this.ad.getChannelName(),
            impressions: stats.impressions,
            empties: stats.empties,
            uptime: uptime.toFixed(1) + 's',
            angle: this.ad.getAngle().toFixed(1),
            visibility: (this.ad.getVisibility() * 100).toFixed(1) + '%',
            viewability: (this.ad.getViewability() * 100).toFixed(1) + '%'
        };
    }

    displayStats(ctx, x, y) {
        const stats = this.getStats();

        ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
        ctx.fillRect(x, y, 200, 140);

        ctx.fillStyle = '#00ff00';
        ctx.font = '12px monospace';
        let lineY = y + 20;

        Object.entries(stats).forEach(([key, value]) => {
            ctx.fillText(`${key}: ${value}`, x + 10, lineY);
            lineY += 18;
        });
    }
}

// Usage
const monitor = new AdMonitor(billboard.ad);

// In render loop
monitor.displayStats(ctx, 10, 10);
```

#### 4. Responsive Billboards

```javascript
// Auto-resize billboards on window resize
function createResponsiveBillboard(config) {
    const billboard = {
        ad: new AnzuAd(config),
        baseWidth: config.width,
        aspectRatio: config.aspectRatio || 16/9,
        scale: 1
    };

    billboard.ad.initialize();

    // Calculate responsive size
    function updateSize() {
        const maxWidth = window.innerWidth * 0.8;
        billboard.scale = Math.min(1, maxWidth / billboard.baseWidth);
        billboard.width = billboard.baseWidth * billboard.scale;
        billboard.height = billboard.width / billboard.aspectRatio;
    }

    updateSize();
    window.addEventListener('resize', updateSize);

    return billboard;
}
```

#### 5. Performance Optimization

```javascript
// Lazy update for off-screen billboards
class OptimizedBillboard {
    constructor(config) {
        this.ad = new AnzuAd(config);
        this.ad.initialize();
        this.isOnScreen = true;
        this.updateInterval = 0;
    }

    update(deltaTime, isVisible) {
        this.isOnScreen = isVisible;

        if (isVisible) {
            // Update every frame when visible
            this.updateVisibility();
        } else {
            // Update less frequently when off-screen
            this.updateInterval += deltaTime;
            if (this.updateInterval >= 1.0) {
                this.updateVisibility();
                this.updateInterval = 0;
            }
        }
    }

    updateVisibility() {
        this.ad.updateVisibility({
            angle: 0,
            visibility: this.isOnScreen ? 0.5 : 0,
            viewability: this.isOnScreen ? 1 : 0
        });
    }

    render(ctx, x, y, width, height) {
        if (!this.isOnScreen) return;

        const canvas = this.ad.getCanvas();
        if (canvas) {
            ctx.drawImage(canvas, x, y, width, height);
        }
    }
}
```

***

### Complete Examples

#### Example 1: Simple Billboard Game

```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Billboard Runner</title>
    <style>
        body { margin: 0; background: #000; overflow: hidden; }
        canvas { display: block; }
        #stats {
            position: absolute;
            top: 10px;
            left: 10px;
            color: #0f0;
            font-family: monospace;
            background: rgba(0,0,0,0.7);
            padding: 10px;
        }
    </style>
</head>
<body>
    <div id="stats"></div>
    <canvas id="game"></canvas>

    <!-- Anzu SDK -->
    <script src="intrinsic-ads-js-sdk/core/SDKLoader.js"></script>
    <script src="intrinsic-ads-js-sdk/utils/AnzuDebug.js"></script>
    <script src="intrinsic-ads-js-sdk/utils/AnzuMath.js"></script>
    <script src="intrinsic-ads-js-sdk/utils/UriSchema.js"></script>
    <script src="intrinsic-ads-js-sdk/core/AnzuCore.js"></script>
    <script src="intrinsic-ads-js-sdk/core/ChannelManager.js"></script>
    <script src="intrinsic-ads-js-sdk/core/RenderManager.js"></script>
    <script src="intrinsic-ads-js-sdk/AnzuAd.js"></script>

    <script>
        const canvas = document.getElementById('game');
        const ctx = canvas.getContext('2d');
        canvas.width = 800;
        canvas.height = 600;

        // Initialize Anzu
        AnzuCore.initialize({
            appKey: "YOUR_ANZU_APP_KEY",
            logLevel: 1
        });

        // Create scrolling billboards
        const billboards = [
            {
                ad: new AnzuAd({
                    channelName: "billboard_1",
                    aspectRatio: 16/9,
                    isClickable: true
                }),
                x: 100,
                y: 200,
                width: 300,
                height: 169
            },
            {
                ad: new AnzuAd({
                    channelName: "billboard_2",
                    aspectRatio: 1,
                    isClickable: true
                }),
                x: 500,
                y: 200,
                width: 200,
                height: 200
            }
        ];

        billboards.forEach(b => b.ad.initialize());

        // Player
        const player = { x: 100, y: 500, width: 30, height: 30, speed: 200 };
        const keys = {};

        window.addEventListener('keydown', e => keys[e.key] = true);
        window.addEventListener('keyup', e => keys[e.key] = false);

        canvas.addEventListener('click', (e) => {
            const rect = canvas.getBoundingClientRect();
            const x = e.clientX - rect.left;
            const y = e.clientY - rect.top;

            billboards.forEach(b => {
                if (x >= b.x && x <= b.x + b.width &&
                    y >= b.y && y <= b.y + b.height) {
                    b.ad.interact();
                }
            });
        });

        let lastTime = performance.now();

        function gameLoop(currentTime) {
            const deltaTime = (currentTime - lastTime) / 1000;
            lastTime = currentTime;

            // Update Anzu
            AnzuCore.update(deltaTime);

            // Update player
            if (keys['ArrowLeft']) player.x -= player.speed * deltaTime;
            if (keys['ArrowRight']) player.x += player.speed * deltaTime;
            if (keys['ArrowUp']) player.y -= player.speed * deltaTime;
            if (keys['ArrowDown']) player.y += player.speed * deltaTime;

            player.x = Math.max(0, Math.min(canvas.width - player.width, player.x));
            player.y = Math.max(0, Math.min(canvas.height - player.height, player.y));

            // Clear
            ctx.fillStyle = '#1a1a2e';
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            // Draw billboards
            billboards.forEach(b => {
                const adCanvas = b.ad.getCanvas();
                if (adCanvas) {
                    ctx.drawImage(adCanvas, b.x, b.y, b.width, b.height);
                }

                // Update visibility
                const area = b.width * b.height;
                const screenArea = canvas.width * canvas.height;
                b.ad.updateVisibility({
                    angle: 0,
                    visibility: area / screenArea,
                    viewability: 1
                });
            });

            // Draw player
            ctx.fillStyle = '#00ff00';
            ctx.fillRect(player.x, player.y, player.width, player.height);

            // Stats
            const stats = billboards.map((b, i) => {
                const s = b.ad.getChannelStats();
                return `Billboard ${i+1}: ${s.impressions} impressions`;
            }).join('<br>');

            document.getElementById('stats').innerHTML = stats;

            requestAnimationFrame(gameLoop);
        }

        requestAnimationFrame(gameLoop);
    </script>
</body>
</html>
```

***

### Best Practices

#### 1. Initialization

* ✅ Initialize `AnzuCore` once at app startup
* ✅ Wait for initialization before creating ads
* ✅ Use appropriate log level for production (2-4)

#### 2. Performance

* ✅ Call `AnzuCore.update()` every frame
* ✅ Reuse canvas references, don't fetch every frame
* ✅ Update visibility only when billboard position/size changes
* ✅ Use `requestAnimationFrame` for smooth rendering

#### 3. Visibility Tracking

* ✅ Report accurate visibility (0-1 range)
* ✅ Set `viewability: 0` when billboard is off-screen
* ✅ Use `AnzuMath` utilities for complex calculations
* ✅ Update visibility every frame or when state changes

#### 4. Memory Management

* ✅ Call `ad.destroy()` when removing billboards
* ✅ Remove event listeners when cleaning up
* ✅ Clear references to destroyed ads

#### 5. Click Handling

* ✅ Implement proper hit testing
* ✅ Only call `interact()` when ad is clickable
* ✅ Verify billboard bounds before triggering interaction

***

### Troubleshooting

#### Ad not displaying

```javascript
// Debug checklist
console.log("Anzu initialized?", AnzuCore.isInitialized());
console.log("Canvas exists?", billboard.getCanvas() !== null);
console.log("Canvas size:", billboard.getTextureSize());
```

#### Visibility not tracking

```javascript
// Verify visibility reporting
const vis = billboard.getVisibility();
const view = billboard.getViewability();
console.log("Visibility:", vis, "Viewability:", view);
```

#### Click not working

```javascript
// Debug click detection
canvas.addEventListener('click', (e) => {
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    console.log("Click at:", x, y);
    console.log("Billboard bounds:", billboard.x, billboard.y, billboard.width, billboard.height);
});
```

***

### Resources

* **Main Documentation**: `README.md`
* **Migration Guide**: `MIGRATION_GUIDE.md`
* **Three.js Guide**: `examples/three-js/README.md`
* **Pixi.js Guide**: `examples/pixi-js/README.md`
* **Phaser Guide**: `examples/phaser-js/README.md`
* **Anzu Dashboard**: <https://dashboard.anzu.io/>

***

### Support

For issues or questions:

1. Check the main `README.md`
2. Review error messages in browser console
3. Verify all SDK files are loaded in correct order
4. Contact Playgama support with your app key
