Skip to content

Instantly share code, notes, and snippets.

@lardratboy
Last active September 30, 2025 06:11
Show Gist options
  • Select an option

  • Save lardratboy/8828c519770b510043fb5c6a5497e41d to your computer and use it in GitHub Desktop.

Select an option

Save lardratboy/8828c519770b510043fb5c6a5497e41d to your computer and use it in GitHub Desktop.
some progress purgatory game
<!DOCTYPE html>
<html>
<head>
<title>Space Shooter</title>
<style>
body { margin: 0; overflow: hidden; }
#score { position: fixed; top: 20px; right: 20px; color: white; font-family: Arial; }
#restart { position: fixed; top: 20px; left: 20px; padding: 10px; }
#instructions { position: fixed; bottom: 20px; left: 20px; color: white; font-family: Arial; }
</style>
</head>
<body>
<div id="score">
Score: <span id="scoreValue">0</span><br>
Level: <span id="levelValue">1</span><br>
Lives: <span id="livesValue">3</span>
</div>
<button id="restart">Restart</button>
<div id="instructions">
WASD or Arrow Keys to move<br>
Space to shoot<br>
Extra life every 10,000 points!
</div>
<div id="gameOver" style="display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-family: Arial; font-size: 48px; text-align: center;">
GAME OVER<br>
<span style="font-size: 24px;">Press Restart to try again</span>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// Enemy type definitions
const ENEMY_TYPES = {
SPHERE: {
name: 'Sphere',
color: 0xff0000, // Red
geometry: () => new THREE.SphereGeometry(0.5, 16, 16),
pattern: 0,
points: 50
},
TETRAHEDRON: {
name: 'Tetrahedron',
color: 0x00ff00, // Green
geometry: () => new THREE.TetrahedronGeometry(0.6),
pattern: 1,
points: 100
},
CUBE: {
name: 'Cube',
color: 0x0000ff, // Blue
geometry: () => new THREE.BoxGeometry(0.8, 0.8, 0.8),
pattern: 2,
points: 150
},
OCTAHEDRON: {
name: 'Octahedron',
color: 0xffff00, // Yellow
geometry: () => new THREE.OctahedronGeometry(0.6),
pattern: 3,
points: 200
},
DODECAHEDRON: {
name: 'Dodecahedron',
color: 0xff00ff, // Magenta
geometry: () => new THREE.DodecahedronGeometry(0.5),
pattern: 4,
points: 250
},
ICOSAHEDRON: {
name: 'Icosahedron',
color: 0x888888, // Medium Gray
geometry: () => new THREE.IcosahedronGeometry(0.6),
pattern: 5,
points: 300,
slowDown: true // Move slower than other enemies
}
};
class GameObject {
constructor(geometry, material, position = { x: 0, y: 0, z: 0 }) {
this.mesh = new THREE.Mesh(geometry, material);
this.mesh.position.set(position.x, position.y, position.z);
this.velocity = { x: 0, y: 0, z: 0 };
}
update() {
this.mesh.position.x += this.velocity.x;
this.mesh.position.y += this.velocity.y;
}
}
class Player extends GameObject {
constructor() {
const geometry = new THREE.ConeGeometry(0.5, 1, 3);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
super(geometry, material);
this.mesh.rotation.z = 0;
this.speed = 0.15;
this.isInvulnerable = false;
this.invulnerableTime = 0;
this.blinkTime = 0;
}
update(deltaTime) {
if (this.isInvulnerable) {
this.invulnerableTime += deltaTime;
this.blinkTime += deltaTime;
// Blink effect
if (this.blinkTime > 0.1) {
this.mesh.visible = !this.mesh.visible;
this.blinkTime = 0;
}
// End invulnerability after 3 seconds
if (this.invulnerableTime > 3) {
this.isInvulnerable = false;
this.mesh.visible = true;
}
}
}
makeInvulnerable() {
this.isInvulnerable = true;
this.invulnerableTime = 0;
this.blinkTime = 0;
}
}
class Enemy extends GameObject {
constructor(position, enemyType, enemyIndex, level, groupParams) {
const geometry = enemyType.geometry();
const material = new THREE.MeshPhongMaterial({
color: enemyType.color,
shininess: 60,
specular: 0x444444
});
super(geometry, material, position);
this.type = enemyType;
this.pattern = enemyType.pattern;
this.enemyIndex = enemyIndex;
this.level = level;
this.groupParams = groupParams; // Parameters for group center movement
// Increase movement speed and radius with level
this.radius = 1 + (level - 1) * 0.15;
// Some enemy types move slower
if (enemyType.slowDown) {
this.speedMultiplier = (1 + (level - 1) * 0.12) * 0.5; // Half speed
} else {
this.speedMultiplier = 1 + (level - 1) * 0.12;
}
this.formationTime = 0;
this.groupTime = 0; // Separate time for group center movement
// Dive attack state
this.isDiving = false;
this.isReturning = false;
this.diveTimer = Math.random() * 10 + 5; // Random delay before first dive
this.returnProgress = 0;
this.returnStartX = 0;
this.returnStartY = 0;
// Set initial offscreen position
const angle = Math.random() * Math.PI * 2;
const distance = 15;
this.startX = Math.cos(angle) * distance;
this.startY = Math.sin(angle) * distance;
this.mesh.position.set(this.startX, this.startY, 0);
// Initial target position
this.initialCenterX = position.x;
this.initialCenterY = position.y;
this.centerX = position.x;
this.centerY = position.y;
// Entry animation
this.entryProgress = 0;
this.isEntering = true;
}
update() {
// Update group center position using ping pong horizontal movement
if (!this.isEntering && !this.isDiving && !this.isReturning) {
this.groupTime += 0.015 * this.speedMultiplier;
const gp = this.groupParams;
// Horizontal ping pong movement
const cycleLength = gp.cycleLength;
const cycleProgress = (this.groupTime % cycleLength) / cycleLength;
const cycleNumber = Math.floor(this.groupTime / cycleLength);
const movingRight = cycleNumber % 2 === 0;
if (movingRight) {
this.centerX = gp.leftBound + (cycleProgress * (gp.rightBound - gp.leftBound));
} else {
this.centerX = gp.rightBound - (cycleProgress * (gp.rightBound - gp.leftBound));
}
// Vertical descent - increases with level
const descentRate = 0.15 + (this.level - 1) * 0.02;
const totalDescent = Math.floor(this.groupTime / cycleLength) * descentRate;
this.centerY = this.initialCenterY - totalDescent;
// Add slight Lissajous wave on top for visual interest
this.centerY += Math.sin(gp.freqY * this.groupTime + gp.phaseY) * gp.amplitudeY;
}
// Entry animation
if (this.isEntering) {
this.entryProgress += 0.05;
if (this.entryProgress >= 1) {
this.isEntering = false;
}
const ease = 1 - Math.pow(1 - this.entryProgress, 3);
this.mesh.position.x = this.startX + (this.centerX - this.startX) * ease;
this.mesh.position.y = this.startY + (this.centerY - this.startY) * ease;
// Orient during entry
const angle = Math.atan2(
this.centerY - this.mesh.position.y,
this.centerX - this.mesh.position.x
);
this.mesh.rotation.z = angle + Math.PI / 2;
return;
}
// Return animation (wrapping back in from behind camera)
if (this.isReturning) {
this.returnProgress += 0.04;
if (this.returnProgress >= 1) {
this.isReturning = false;
this.isDiving = false;
this.diveTimer = Math.random() * 15 + 8; // Reset dive timer
}
const ease = 1 - Math.pow(1 - this.returnProgress, 3);
this.mesh.position.x = this.returnStartX + (this.centerX - this.returnStartX) * ease;
this.mesh.position.y = this.returnStartY + (this.centerY - this.returnStartY) * ease;
this.mesh.position.z = 5 * (1 - ease); // Come in from behind camera
// Orient toward center
const angle = Math.atan2(
this.centerY - this.mesh.position.y,
this.centerX - this.mesh.position.x
);
this.mesh.rotation.z = angle + Math.PI / 2;
return;
}
// Dive attack
if (this.isDiving) {
this.mesh.position.y -= 0.25; // Fast downward movement
// When off screen, start return animation
if (this.mesh.position.y < -6) {
this.isReturning = true;
this.returnProgress = 0;
// Set return start position (from behind camera, random angle)
const angle = Math.random() * Math.PI * 2;
const distance = 12;
this.returnStartX = Math.cos(angle) * distance;
this.returnStartY = Math.sin(angle) * distance;
this.mesh.position.set(this.returnStartX, this.returnStartY, 5);
}
// Keep pointing down during dive
this.mesh.rotation.z = Math.PI;
this.mesh.rotation.x += 0.05;
return;
}
// Check for dive initiation
if (!this.isDiving && !this.isReturning) {
this.diveTimer -= 0.016;
if (this.diveTimer <= 0) {
// Random chance to dive (20% per check when timer expires)
if (Math.random() < 0.2) {
this.isDiving = true;
} else {
this.diveTimer = Math.random() * 5 + 3; // Try again soon
}
}
}
this.formationTime += 0.02 * this.speedMultiplier;
const prevX = this.mesh.position.x;
const prevY = this.mesh.position.y;
// Each shape type has its own movement pattern
switch(this.pattern) {
case 0: // Sphere - Simple circular pattern
const t0 = this.formationTime + (this.enemyIndex * Math.PI / 3);
this.mesh.position.x = this.centerX + Math.cos(t0) * this.radius;
this.mesh.position.y = this.centerY + Math.sin(t0) * this.radius;
break;
case 1: // Tetrahedron - Figure-8 pattern
const t1 = this.formationTime + (this.enemyIndex * Math.PI / 4);
this.mesh.position.x = this.centerX + Math.sin(t1) * this.radius * 1.5;
this.mesh.position.y = this.centerY + Math.sin(t1 * 2) * this.radius * 0.8;
break;
case 2: // Cube - Lissajous pattern (2:3 ratio)
const t2 = this.formationTime + this.enemyIndex;
this.mesh.position.x = this.centerX + Math.sin(2 * t2) * this.radius;
this.mesh.position.y = this.centerY + Math.sin(3 * t2) * this.radius;
break;
case 3: // Octahedron - Trefoil knot
const t3 = this.formationTime + (this.enemyIndex * Math.PI / 2);
this.mesh.position.x = this.centerX + (Math.sin(t3) + 2 * Math.sin(2 * t3)) * this.radius * 0.4;
this.mesh.position.y = this.centerY + (Math.cos(t3) - 2 * Math.cos(2 * t3)) * this.radius * 0.4;
break;
case 4: // Dodecahedron - Spiral with dive
const t4 = this.formationTime + this.enemyIndex;
const spiralR = this.radius * (0.5 + 0.5 * Math.sin(t4 * 0.5));
const dive = Math.sin(t4 * 0.3) * 1.5;
this.mesh.position.x = this.centerX + Math.cos(t4 * 1.5) * spiralR;
this.mesh.position.y = this.centerY + Math.sin(t4 * 1.5) * spiralR + dive;
break;
case 5: // Icosahedron - Space Invader zigzag pattern
const t5 = this.formationTime;
const zigzagSpeed = 0.8;
const zigzagWidth = this.radius * 2;
// Calculate horizontal position (zigzag back and forth)
const cycleLength = 4; // Time for one complete left-right-down cycle
const cycleProgress = (t5 % cycleLength) / cycleLength;
// Determine if moving right or left
const moveRight = Math.floor(t5 / cycleLength) % 2 === 0;
const horizontalProgress = cycleProgress;
if (moveRight) {
this.mesh.position.x = this.centerX - zigzagWidth + (horizontalProgress * zigzagWidth * 2);
} else {
this.mesh.position.x = this.centerX + zigzagWidth - (horizontalProgress * zigzagWidth * 2);
}
// Gradual descent
const dropAmount = Math.floor(t5 / cycleLength) * 0.3;
this.mesh.position.y = this.centerY - dropAmount;
// Keep rotation stiff/minimal for Space Invader feel
this.mesh.rotation.x = 0;
this.mesh.rotation.y += 0.01;
break;
}
// Orient based on movement direction
const dx = this.mesh.position.x - prevX;
const dy = this.mesh.position.y - prevY;
if (Math.abs(dx) > 0.001 || Math.abs(dy) > 0.001) {
const angle = Math.atan2(dy, dx);
this.mesh.rotation.z = angle + Math.PI / 2;
}
// Rotation on multiple axes for visual interest
// Icosahedron (pattern 5) handles its own rotation
if (this.pattern !== 5) {
this.mesh.rotation.x += 0.02 * this.speedMultiplier;
this.mesh.rotation.y += 0.015 * this.speedMultiplier;
}
}
}
class Bullet extends GameObject {
constructor(position) {
const geometry = new THREE.SphereGeometry(0.1);
const material = new THREE.MeshBasicMaterial({ color: 0xffff00 });
super(geometry, material, position);
this.velocity.y = 0.3;
}
}
class ExplosionDebris extends GameObject {
constructor(position) {
const size = 0.1 + Math.random() * 0.15;
const geometry = new THREE.BoxGeometry(size, size, size);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
super(geometry, material, position);
// Random velocity in all directions
const angle = Math.random() * Math.PI * 2;
const speed = 0.1 + Math.random() * 0.15;
this.velocity.x = Math.cos(angle) * speed;
this.velocity.y = Math.sin(angle) * speed;
this.velocity.z = (Math.random() - 0.5) * 0.1;
// Rotation velocities
this.rotationVelocity = {
x: (Math.random() - 0.5) * 0.2,
y: (Math.random() - 0.5) * 0.2,
z: (Math.random() - 0.5) * 0.2
};
this.lifetime = 0;
this.maxLifetime = 1.5;
}
update() {
super.update();
this.mesh.rotation.x += this.rotationVelocity.x;
this.mesh.rotation.y += this.rotationVelocity.y;
this.mesh.rotation.z += this.rotationVelocity.z;
this.lifetime += 0.016;
// Fade out
const alpha = 1 - (this.lifetime / this.maxLifetime);
this.mesh.material.opacity = alpha;
return this.lifetime >= this.maxLifetime;
}
}
class Game {
constructor() {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(this.renderer.domElement);
// Add lighting
const ambientLight = new THREE.AmbientLight(0x333333);
const mainLight = new THREE.DirectionalLight(0xffffff, 1);
mainLight.position.set(5, 5, 7);
this.scene.add(ambientLight, mainLight);
// Create starfield
this.stars = [];
const starGeometry = new THREE.SphereGeometry(0.05);
const starMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
for(let i = 0; i < 200; i++) {
const star = new THREE.Mesh(starGeometry, starMaterial);
star.position.set(
(Math.random() - 0.5) * 40,
(Math.random() - 0.5) * 40,
(Math.random() - 0.5) * 20
);
this.stars.push(star);
this.scene.add(star);
}
// Add nebula-like effects
const nebulaGeometry = new THREE.PlaneGeometry(40, 40);
const nebulaMaterial = new THREE.MeshBasicMaterial({
color: 0x4444ff,
transparent: true,
opacity: 0.1
});
for(let i = 0; i < 3; i++) {
const nebula = new THREE.Mesh(nebulaGeometry, nebulaMaterial.clone());
nebula.position.z = -10 - i * 5;
nebula.material.color.setHSL(Math.random(), 0.6, 0.6);
this.scene.add(nebula);
}
this.camera.position.z = 10;
this.camera.lookAt(0, 0, 0);
this.player = new Player();
this.scene.add(this.player.mesh);
this.player.mesh.position.y = -4;
this.enemies = [];
this.bullets = [];
this.explosionDebris = [];
this.score = 0;
this.level = 1;
this.lives = 3;
this.lastScoreForExtraLife = 0;
this.isGameOver = false;
this.createEnemyWave();
this.setupControls();
this.animate();
}
createEnemyWave() {
// Determine which enemy types to spawn based on level
const enemyTypeArray = Object.values(ENEMY_TYPES);
const maxTypeIndex = Math.min(Math.floor((this.level - 1) / 2) + 1, enemyTypeArray.length);
const availableTypes = enemyTypeArray.slice(0, maxTypeIndex);
// Increase number of enemies with level
const enemiesPerGroup = Math.min(3 + Math.floor(this.level / 3), 6);
// Determine number of groups per type (can have multiple groups of same type)
const groupsPerType = Math.min(1 + Math.floor(this.level / 4), 3);
const allGroups = [];
// Create groups for each available enemy type
availableTypes.forEach((enemyType, typeIndex) => {
for (let groupNum = 0; groupNum < groupsPerType; groupNum++) {
// Generate unique parameters for each group's ping pong movement
const levelSeed = this.level * 7 + typeIndex * 13 + groupNum * 19;
// Horizontal bounds for ping pong (varies per group)
const boundOffset = (levelSeed % 3) * 0.5;
const leftBound = -4 + boundOffset;
const rightBound = 4 - boundOffset;
// Cycle length (time to traverse left to right or right to left)
const cycleLength = 5 - (this.level % 3) * 0.5; // Faster at higher levels
// Vertical wave parameters (adds slight bobbing motion)
const freqY = 1 + ((levelSeed + 5) % 4) * 0.5;
const phaseY = ((levelSeed + 3) % 8) * Math.PI / 4;
const amplitudeY = 0.2 + (levelSeed % 3) * 0.1;
allGroups.push({
type: enemyType,
params: {
leftBound,
rightBound,
cycleLength,
freqY,
phaseY,
amplitudeY
},
enemiesPerGroup
});
}
});
// Space out groups across the screen
const spacing = 2.5;
const totalWidth = (allGroups.length - 1) * spacing;
allGroups.forEach((group, groupIndex) => {
const baseX = (groupIndex * spacing) - (totalWidth / 2);
for (let i = 0; i < group.enemiesPerGroup; i++) {
const enemy = new Enemy({
x: baseX,
y: 3,
z: 0
}, group.type, i, this.level, group.params);
this.enemies.push(enemy);
this.scene.add(enemy.mesh);
}
});
}
setupControls() {
this.keys = {};
document.addEventListener('keydown', (e) => this.keys[e.code] = true);
document.addEventListener('keyup', (e) => this.keys[e.code] = false);
document.addEventListener('keydown', (e) => {
if (e.code === 'Space' && !this.keys.shooting) {
this.shoot();
this.keys.shooting = true;
}
});
document.addEventListener('keyup', (e) => {
if (e.code === 'Space') this.keys.shooting = false;
});
document.getElementById('restart').addEventListener('click', () => this.restart());
}
shoot() {
if (this.isGameOver) return;
const bullet = new Bullet({
x: this.player.mesh.position.x,
y: this.player.mesh.position.y + 0.5,
z: 0
});
this.bullets.push(bullet);
this.scene.add(bullet.mesh);
}
handleMovement() {
if (this.isGameOver) return;
if (this.keys['ArrowLeft'] || this.keys['KeyA'])
this.player.mesh.position.x = Math.max(-5, this.player.mesh.position.x - this.player.speed);
if (this.keys['ArrowRight'] || this.keys['KeyD'])
this.player.mesh.position.x = Math.min(5, this.player.mesh.position.x + this.player.speed);
}
checkCollisions() {
// Check bullet-enemy collisions
for (let i = this.bullets.length - 1; i >= 0; i--) {
const bullet = this.bullets[i];
for (let j = this.enemies.length - 1; j >= 0; j--) {
const enemy = this.enemies[j];
const dx = bullet.mesh.position.x - enemy.mesh.position.x;
const dy = bullet.mesh.position.y - enemy.mesh.position.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 0.6) {
this.scene.remove(bullet.mesh);
this.scene.remove(enemy.mesh);
this.bullets.splice(i, 1);
this.enemies.splice(j, 1);
this.score += enemy.type.points;
document.getElementById('scoreValue').textContent = this.score;
// Check for extra life
if (Math.floor(this.score / 10000) > Math.floor(this.lastScoreForExtraLife / 10000)) {
this.lives++;
document.getElementById('livesValue').textContent = this.lives;
this.lastScoreForExtraLife = this.score;
}
break;
}
}
}
// Check player-enemy collisions
if (!this.player.isInvulnerable && !this.isGameOver) {
for (let i = this.enemies.length - 1; i >= 0; i--) {
const enemy = this.enemies[i];
const dx = this.player.mesh.position.x - enemy.mesh.position.x;
const dy = this.player.mesh.position.y - enemy.mesh.position.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 0.8) {
this.playerHit();
this.scene.remove(enemy.mesh);
this.enemies.splice(i, 1);
break;
}
}
}
}
playerHit() {
this.lives--;
document.getElementById('livesValue').textContent = this.lives;
// Create explosion
this.explodePlayer();
if (this.lives <= 0) {
this.gameOver();
} else {
// Respawn player after delay
setTimeout(() => this.respawnPlayer(), 1500);
}
}
explodePlayer() {
const pos = this.player.mesh.position;
// Hide player temporarily
this.player.mesh.visible = false;
// Create debris
for (let i = 0; i < 20; i++) {
const debris = new ExplosionDebris({
x: pos.x,
y: pos.y,
z: pos.z
});
debris.mesh.material.transparent = true;
this.explosionDebris.push(debris);
this.scene.add(debris.mesh);
}
}
respawnPlayer() {
if (this.lives > 0) {
this.player.mesh.position.set(0, -4, 0);
this.player.mesh.visible = true;
this.player.makeInvulnerable();
}
}
gameOver() {
this.isGameOver = true;
document.getElementById('gameOver').style.display = 'block';
}
restart() {
this.score = 0;
this.level = 1;
this.lives = 3;
this.lastScoreForExtraLife = 0;
this.isGameOver = false;
document.getElementById('scoreValue').textContent = this.score;
document.getElementById('levelValue').textContent = this.level;
document.getElementById('livesValue').textContent = this.lives;
document.getElementById('gameOver').style.display = 'none';
// Remove all enemies, bullets, and debris
this.enemies.forEach(enemy => this.scene.remove(enemy.mesh));
this.bullets.forEach(bullet => this.scene.remove(bullet.mesh));
this.explosionDebris.forEach(debris => this.scene.remove(debris.mesh));
this.enemies = [];
this.bullets = [];
this.explosionDebris = [];
// Reset player position
this.player.mesh.position.set(0, -4, 0);
this.player.mesh.visible = true;
this.player.isInvulnerable = false;
// Create new enemy wave
this.createEnemyWave();
}
animate() {
requestAnimationFrame(() => this.animate());
// Parallax star movement
this.stars.forEach(star => {
star.position.z += 0.05;
if(star.position.z > 10) star.position.z = -20;
});
this.handleMovement();
// Update player
this.player.update(0.016);
this.enemies.forEach(enemy => enemy.update());
this.bullets.forEach(bullet => bullet.update());
// Update explosion debris
for (let i = this.explosionDebris.length - 1; i >= 0; i--) {
const shouldRemove = this.explosionDebris[i].update();
if (shouldRemove) {
this.scene.remove(this.explosionDebris[i].mesh);
this.explosionDebris.splice(i, 1);
}
}
// Remove bullets that are out of bounds
for (let i = this.bullets.length - 1; i >= 0; i--) {
if (this.bullets[i].mesh.position.y > 6) {
this.scene.remove(this.bullets[i].mesh);
this.bullets.splice(i, 1);
}
}
// Remove enemies that have descended too far (but not diving enemies)
for (let i = this.enemies.length - 1; i >= 0; i--) {
if (this.enemies[i].mesh.position.y < -5 && !this.enemies[i].isDiving && !this.enemies[i].isReturning) {
this.scene.remove(this.enemies[i].mesh);
this.enemies.splice(i, 1);
}
}
this.checkCollisions();
// Check if level is complete
if (this.enemies.length === 0 && !this.isGameOver) {
this.level++;
document.getElementById('levelValue').textContent = this.level;
this.createEnemyWave();
}
this.renderer.render(this.scene, this.camera);
}
}
// Start the game
window.addEventListener('load', () => {
const game = new Game();
// Handle window resize
window.addEventListener('resize', () => {
game.camera.aspect = window.innerWidth / window.innerHeight;
game.camera.updateProjectionMatrix();
game.renderer.setSize(window.innerWidth, window.innerHeight);
});
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment