← back to Model Arena
data/artifacts/035b7009e366/kimi.html
934 lines
<!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>Designer Wallcoverings - The Roll</title>
<style>
:root {
/* Luxury Design Tokens */
--bg: #0d0d0f;
--surface: #17171c;
--surface-elevated: #1e1e26;
--ink: #f5f2ea;
--muted: #8a8578;
--accent: #c9a961;
--accent-glow: rgba(201, 169, 97, 0.3);
--line: rgba(245, 242, 234, 0.12);
--shadow: rgba(0, 0, 0, 0.6);
--shadow-soft: 0 8px 32px rgba(0, 0, 0, 0.4);
/* Typography */
--font-display: 'Didot', 'Bodoni MT', 'Playfair Display', Georgia, serif;
--font-body: 'Avenir Next', 'Century Gothic', Futura, 'Segoe UI', sans-serif;
/* Motion */
--ease-swift: cubic-bezier(.22,.9,.24,1);
--ease-overshoot: cubic-bezier(.34,1.56,.64,1);
--ease-quint: cubic-bezier(.22,1,.36,1);
/* Spacing */
--space-xs: 4px;
--space-sm: 8px;
--space-md: 16px;
--space-lg: 24px;
--space-xl: 40px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-body);
background: var(--bg);
color: var(--ink);
overflow: hidden;
touch-action: none;
user-select: none;
-webkit-user-select: none;
}
/* Fixed Top Bar - Logo Zone */
.top-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 72px;
background: linear-gradient(180deg, var(--bg) 0%, rgba(13,13,15,0.95) 100%);
border-bottom: 1px solid var(--line);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--space-lg);
z-index: 100;
backdrop-filter: blur(8px);
}
.wordmark {
font-family: var(--font-display);
font-size: 22px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--ink);
font-weight: 400;
line-height: 1.1;
}
.score-board {
display: flex;
gap: var(--space-lg);
align-items: center;
}
.score-item {
font-family: var(--font-body);
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
display: flex;
flex-direction: column;
align-items: flex-end;
}
.score-value {
font-size: 18px;
color: var(--accent);
font-weight: 600;
letter-spacing: 0.05em;
}
/* Game Container */
#game-container {
position: fixed;
top: 72px;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
canvas {
display: block;
box-shadow: var(--shadow-soft);
border: 1px solid var(--line);
background: var(--surface);
}
/* Overlays */
.overlay {
position: fixed;
top: 72px;
left: 0;
right: 0;
bottom: 0;
background: rgba(13, 13, 15, 0.92);
backdrop-filter: blur(12px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 50;
opacity: 0;
pointer-events: none;
transition: opacity 0.4s var(--ease-quint);
}
.overlay.active {
opacity: 1;
pointer-events: all;
}
.overlay-content {
text-align: center;
max-width: 480px;
padding: var(--space-xl);
animation: slideUp 0.6s var(--ease-overshoot) forwards;
}
@keyframes slideUp {
from { transform: translateY(40px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.overlay h2 {
font-family: var(--font-display);
font-size: 42px;
letter-spacing: -0.02em;
color: var(--ink);
margin-bottom: var(--space-md);
font-weight: 400;
}
.overlay p {
font-size: 14px;
color: var(--muted);
line-height: 1.6;
margin-bottom: var(--space-xl);
letter-spacing: 0.02em;
}
.btn {
font-family: var(--font-body);
font-size: 12px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--bg);
background: var(--accent);
border: none;
padding: 16px 40px;
cursor: pointer;
transition: all 0.3s var(--ease-swift);
box-shadow: 0 4px 24px var(--accent-glow);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 32px var(--accent-glow);
}
.btn:active {
transform: translateY(0);
}
.final-score {
font-family: var(--font-display);
font-size: 64px;
color: var(--accent);
margin: var(--space-md) 0;
letter-spacing: -0.02em;
}
.best-score {
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: var(--space-xl);
}
/* Instructions */
.instructions {
position: absolute;
bottom: var(--space-xl);
left: 50%;
transform: translateX(-50%);
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
opacity: 0.7;
}
/* Visual indicator for controls */
.controls-hint {
display: flex;
gap: var(--space-md);
margin-top: var(--space-lg);
justify-content: center;
}
.key {
width: 44px;
height: 44px;
border: 1px solid var(--line);
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
background: var(--surface-elevated);
}
/* Responsive */
@media (max-width: 640px) {
.wordmark {
font-size: 16px;
letter-spacing: 0.14em;
line-height: 1.2;
}
.top-bar {
padding: 0 var(--space-md);
height: 64px;
}
#game-container {
top: 64px;
}
.overlay {
top: 64px;
}
.overlay h2 {
font-size: 32px;
}
}
</style>
</head>
<body>
<!-- Top Bar -->
<header class="top-bar">
<div class="wordmark">Designer Wallcoverings</div>
<div class="score-board">
<div class="score-item">
<span>Current</span>
<span class="score-value" id="current-score">0</span>
</div>
<div class="score-item">
<span>Best</span>
<span class="score-value" id="best-score">0</span>
</div>
</div>
</header>
<!-- Game Container -->
<div id="game-container">
<canvas id="game"></canvas>
<div class="instructions">Tap or press space to jump</div>
</div>
<!-- Start Overlay -->
<div class="overlay active" id="start-overlay">
<div class="overlay-content">
<h2>The Roll</h2>
<p>Guide the roller through the atelier. Collect rolls of distinguished wallcoverings while avoiding obstacles. Speed increases with each pattern collected.</p>
<button class="btn" onclick="startGame()">Begin Journey</button>
<div class="controls-hint">
<div class="key">Space</div>
<div class="key">Tap</div>
</div>
</div>
</div>
<!-- Game Over Overlay -->
<div class="overlay" id="game-over-overlay">
<div class="overlay-content">
<h2>Atelier Closed</h2>
<div class="final-score" id="final-score">0</div>
<div class="best-score" id="final-best">Best: 0</div>
<button class="btn" onclick="restartGame()">New Collection</button>
</div>
</div>
<script>
// Game Setup
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const startOverlay = document.getElementById('start-overlay');
const gameOverOverlay = document.getElementById('game-over-overlay');
const currentScoreEl = document.getElementById('current-score');
const bestScoreEl = document.getElementById('best-score');
const finalScoreEl = document.getElementById('final-score');
const finalBestEl = document.getElementById('final-best');
// Game Constants
const CANVAS_WIDTH = 800;
const CANVAS_HEIGHT = 450;
const GROUND_Y = 380;
const GRAVITY = 0.6;
const JUMP_FORCE = -12;
const BASE_SPEED = 5;
const MAX_SPEED = 12;
// Set canvas size
function resizeCanvas() {
const container = document.getElementById('game-container');
const maxWidth = Math.min(container.clientWidth - 32, CANVAS_WIDTH);
const scale = maxWidth / CANVAS_WIDTH;
canvas.style.width = maxWidth + 'px';
canvas.style.height = (CANVAS_HEIGHT * scale) + 'px';
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Game State
let gameState = 'start'; // start, playing, gameover
let score = 0;
let bestScore = localStorage.getItem('dw_bestScore') || 0;
bestScoreEl.textContent = bestScore;
let speed = BASE_SPEED;
let frame = 0;
let distance = 0;
// Player (Paint Roller)
const player = {
x: 100,
y: GROUND_Y - 35,
width: 50,
height: 50,
vy: 0,
jumping: false,
rotation: 0,
color: '#c9a961'
};
// Arrays for game objects
let collectibles = [];
let obstacles = [];
let particles = [];
let wallPatterns = [];
// Wallpaper pattern definitions (luxury motifs)
const patterns = [
{ name: 'herringbone', color: '#c9a961', type: 'geometric' },
{ name: 'stripe', color: '#8a8578', type: 'classic' },
{ name: 'damask', color: '#7d6a45', type: 'ornate' },
{ name: 'geo', color: '#f5f2ea', type: 'modern' }
];
// Initialize wall patterns
function initWallPatterns() {
wallPatterns = [];
for (let i = 0; i < 20; i++) {
wallPatterns.push({
x: i * 100,
pattern: patterns[Math.floor(Math.random() * patterns.length)]
});
}
}
// Input Handling
function jump() {
if (gameState !== 'playing') return;
if (!player.jumping) {
player.vy = JUMP_FORCE;
player.jumping = true;
createJumpParticles();
}
}
document.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
if (gameState === 'start') startGame();
else if (gameState === 'gameover') restartGame();
else jump();
}
});
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
if (gameState === 'start') startGame();
else if (gameState === 'gameover') restartGame();
else jump();
});
canvas.addEventListener('mousedown', (e) => {
if (gameState === 'start') startGame();
else if (gameState === 'gameover') restartGame();
else jump();
});
// Particle System
class Particle {
constructor(x, y, color, speed) {
this.x = x;
this.y = y;
this.vx = (Math.random() - 0.5) * speed;
this.vy = (Math.random() - 0.5) * speed;
this.life = 1;
this.decay = 0.02 + Math.random() * 0.02;
this.color = color;
this.size = 2 + Math.random() * 4;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.life -= this.decay;
this.size *= 0.98;
}
draw() {
ctx.save();
ctx.globalAlpha = this.life;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
function createJumpParticles() {
for (let i = 0; i < 8; i++) {
particles.push(new Particle(player.x + 25, player.y + 50, '#c9a961', 3));
}
}
function createCollectParticles(x, y, color) {
for (let i = 0; i < 12; i++) {
particles.push(new Particle(x, y, color, 5));
}
}
function createCrashParticles(x, y) {
for (let i = 0; i < 20; i++) {
particles.push(new Particle(x, y, '#8a8578', 6));
}
}
// Collectible (Wallpaper Roll)
class WallpaperRoll {
constructor(x) {
this.x = x;
this.y = GROUND_Y - 30 - (Math.random() > 0.5 ? 0 : 80);
this.width = 30;
this.height = 40;
this.pattern = patterns[Math.floor(Math.random() * patterns.length)];
this.collected = false;
this.bobOffset = Math.random() * Math.PI * 2;
}
update() {
this.x -= speed;
this.bobOffset += 0.05;
}
draw() {
if (this.collected) return;
const bob = Math.sin(this.bobOffset) * 3;
const y = this.y + bob;
ctx.save();
// Roll shadow
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.beginPath();
ctx.ellipse(this.x + 15, y + 38, 12, 4, 0, 0, Math.PI * 2);
ctx.fill();
// Roll body
ctx.fillStyle = '#1e1e26';
ctx.fillRect(this.x + 5, y, 20, 35);
// Pattern preview on roll
ctx.fillStyle = this.pattern.color;
if (this.pattern.type === 'geometric') {
// Herringbone pattern
ctx.beginPath();
ctx.moveTo(this.x + 8, y + 5);
ctx.lineTo(this.x + 12, y + 10);
ctx.lineTo(this.x + 16, y + 5);
ctx.lineTo(this.x + 20, y + 10);
ctx.stroke();
} else if (this.pattern.type === 'classic') {
// Stripe pattern
ctx.fillRect(this.x + 8, y + 5, 2, 25);
ctx.fillRect(this.x + 14, y + 5, 2, 25);
ctx.fillRect(this.x + 20, y + 5, 2, 25);
} else if (this.pattern.type === 'ornate') {
// Damask approximation
ctx.beginPath();
ctx.arc(this.x + 15, y + 17, 6, 0, Math.PI * 2);
ctx.fill();
} else {
// Modern geometric
ctx.fillRect(this.x + 8, y + 8, 14, 14);
}
// Roll ends
ctx.fillStyle = '#f5f2ea';
ctx.beginPath();
ctx.ellipse(this.x + 15, y, 10, 4, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#8a8578';
ctx.beginPath();
ctx.ellipse(this.x + 15, y + 35, 10, 4, 0, 0, Math.PI * 2);
ctx.fill();
// Glow for visibility
ctx.shadowColor = this.pattern.color;
ctx.shadowBlur = 10;
ctx.strokeStyle = this.pattern.color;
ctx.lineWidth = 1;
ctx.strokeRect(this.x + 5, y, 20, 35);
ctx.shadowBlur = 0;
ctx.restore();
}
}
// Obstacle
class Obstacle {
constructor(x) {
this.x = x;
this.y = GROUND_Y - 35;
this.width = 40;
this.height = 35;
this.type = Math.random() > 0.5 ? 'spill' : 'ladder';
}
update() {
this.x -= speed;
}
draw() {
ctx.save();
if (this.type === 'spill') {
// Paint spill obstacle
ctx.fillStyle = '#17171c';
ctx.beginPath();
ctx.ellipse(this.x + 20, this.y + 30, 20, 10, 0, 0, Math.PI * 2);
ctx.fill();
// Spill droplets
ctx.fillStyle = '#8a8578';
for (let i = 0; i < 5; i++) {
const dx = Math.sin(i * 1.5) * 12;
const dy = Math.cos(i * 2) * 5;
ctx.beginPath();
ctx.arc(this.x + 20 + dx, this.y + 25 + dy, 3 + i, 0, Math.PI * 2);
ctx.fill();
}
} else {
// Fallen ladder/pole
ctx.fillStyle = '#7d6a45';
ctx.fillRect(this.x, this.y + 20, 40, 6);
// Rungs
ctx.fillStyle = '#5a4a32';
for (let i = 0; i < 4; i++) {
ctx.fillRect(this.x + 5 + i * 10, this.y + 15, 4, 16);
}
}
ctx.restore();
}
}
// Draw Functions
function drawBackground() {
// Wall gradient
const gradient = ctx.createLinearGradient(0, 0, 0, CANVAS_HEIGHT);
gradient.addColorStop(0, '#0d0d0f');
gradient.addColorStop(1, '#17171c');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// Moving wall patterns
ctx.save();
ctx.globalAlpha = 0.08;
wallPatterns.forEach((wp, i) => {
wp.x -= speed * 0.3;
if (wp.x < -100) {
wp.x = CANVAS_WIDTH + 50;
wp.pattern = patterns[Math.floor(Math.random() * patterns.length)];
}
ctx.fillStyle = wp.pattern.color;
if (wp.pattern.type === 'geometric') {
// Chevron pattern
ctx.beginPath();
for (let j = 0; j < 3; j++) {
const px = wp.x + j * 30;
ctx.moveTo(px, 50);
ctx.lineTo(px + 15, 80);
ctx.lineTo(px + 30, 50);
}
ctx.stroke();
} else if (wp.pattern.type === 'classic') {
// Vertical stripes
for (let j = 0; j < 3; j++) {
ctx.fillRect(wp.x + j * 25, 0, 4, CANVAS_HEIGHT);
}
} else if (wp.pattern.type === 'ornate') {
// Ornamental circles
ctx.beginPath();
ctx.arc(wp.x + 40, 100, 30, 0, Math.PI * 2);
ctx.stroke();
} else {
// Grid
ctx.strokeRect(wp.x, 60, 60, 80);
ctx.beginPath();
ctx.moveTo(wp.x, 60);
ctx.lineTo(wp.x + 60, 140);
ctx.moveTo(wp.x + 60, 60);
ctx.lineTo(wp.x, 140);
ctx.stroke();
}
});
ctx.restore();
// Baseboard
ctx.fillStyle = '#1e1e26';
ctx.fillRect(0, GROUND_Y, CANVAS_WIDTH, CANVAS_HEIGHT - GROUND_Y);
// Floor detail
ctx.strokeStyle = 'rgba(245, 242, 234, 0.05)';
ctx.lineWidth = 1;
for (let i = 0; i < CANVAS_WIDTH; i += 60) {
const offset = (distance * 0.5) % 60;
ctx.beginPath();
ctx.moveTo(i - offset, GROUND_Y);
ctx.lineTo(i - offset - 30, CANVAS_HEIGHT);
ctx.stroke();
}
}
function drawPlayer() {
ctx.save();
// Shadow
const shadowScale = 1 - (GROUND_Y - player.y) / 200;
ctx.fillStyle = 'rgba(0,0,0,0.4)';
ctx.beginPath();
ctx.ellipse(player.x + 25, GROUND_Y - 5, 20 * shadowScale, 6 * shadowScale, 0, 0, Math.PI * 2);
ctx.fill();
ctx.translate(player.x + 25, player.y + 25);
ctx.rotate(player.rotation);
// Roller handle (metal)
ctx.fillStyle = '#8a8578';
ctx.fillRect(-5, -20, 10, 25);
// Handle grip
ctx.fillStyle = '#7d6a45';
ctx.fillRect(-6, -25, 12, 8);
// Roller frame
ctx.strokeStyle = '#c9a961';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, 5);
ctx.lineTo(0, 15);
ctx.lineTo(-20, 20);
ctx.stroke();
// Roller cylinder
const rollerGradient = ctx.createLinearGradient(-20, -10, 20, 10);
rollerGradient.addColorStop(0, '#1e1e26');
rollerGradient.addColorStop(0.3, '#c9a961');
rollerGradient.addColorStop(0.7, '#c9a961');
rollerGradient.addColorStop(1, '#1e1e26');
ctx.fillStyle = rollerGradient;
ctx.beginPath();
ctx.ellipse(0, 20, 22, 14, 0, 0, Math.PI * 2);
ctx.fill();
// Roller texture lines (rotate with speed)
ctx.strokeStyle = 'rgba(13,13,15,0.5)';
ctx.lineWidth = 1;
for (let i = -15; i < 15; i += 5) {
const lineX = i + (distance * 2) % 5;
ctx.beginPath();
ctx.moveTo(lineX, 12);
ctx.lineTo(lineX, 28);
ctx.stroke();
}
ctx.restore();
}
function updatePlayer() {
// Apply gravity
player.vy += GRAVITY;
player.y += player.vy;
// Ground collision
if (player.y >= GROUND_Y - 35) {
player.y = GROUND_Y - 35;
player.vy = 0;
player.jumping = false;
}
// Rotate based on movement
if (player.jumping) {
player.rotation += 0.1;
} else {
player.rotation += speed * 0.03;
}
}
function checkCollisions() {
const playerRect = {
left: player.x + 10,
right: player.x + 40,
top: player.y + 10,
bottom: player.y + 45
};
// Check collectibles
collectibles.forEach(c => {
if (c.collected) return;
const cRect = {
left: c.x + 5,
right: c.x + 25,
top: c.y,
bottom: c.y + 35
};
if (playerRect.left < cRect.right &&
playerRect.right > cRect.left &&
playerRect.top < cRect.bottom &&
playerRect.bottom > cRect.top) {
c.collected = true;
score += 10;
distance += 50;
speed = Math.min(speed + 0.1, MAX_SPEED);
createCollectParticles(c.x + 15, c.y + 20, c.pattern.color);
currentScoreEl.textContent = score;
}
});
// Check obstacles
obstacles.forEach(o => {
const oRect = {
left: o.x + 5,
right: o.x + 35,
top: o.y + 5,
bottom: o.y + 30
};
if (playerRect.left < oRect.right &&
playerRect.right > oRect.left &&
playerRect.top < oRect.bottom &&
playerRect.bottom > oRect.top) {
gameOver();
}
});
}
function spawnObjects() {
// Spawn collectibles
if (frame % 90 === 0) {
collectibles.push(new WallpaperRoll(CANVAS_WIDTH + 50));
}
// Spawn obstacles
if (frame % 140 === 0 && Math.random() > 0.3) {
obstacles.push(new Obstacle(CANVAS_WIDTH + 50));
}
}
function updateObjects() {
// Update collectibles
collectibles = collectibles.filter(c => {
c.update();
return c.x > -50 && !c.collected;
});
// Update obstacles
obstacles = obstacles.filter(o => {
o.update();
return o.x > -50;
});
// Update particles
particles = particles.filter(p => {
p.update();
return p.life > 0;
});
}
function drawObjects() {
// Draw obstacles behind player
obstacles.forEach(o => o.draw());
// Draw collectibles
collectibles.forEach(c => c.draw());
// Draw particles
particles.forEach(p => p.draw());
}
function update() {
if (gameState !== 'playing') return;
frame++;
distance += speed;
// Passive score for distance
if (frame % 10 === 0) {
score++;
currentScoreEl.textContent = score;
}
updatePlayer();
spawnObjects();
updateObjects();
checkCollisions();
}
function draw() {
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
drawBackground();
drawObjects();
drawPlayer();
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
function startGame() {
gameState = 'playing';
score = 0;
speed = BASE_SPEED;
frame = 0;
distance = 0;
player.y = GROUND_Y - 35;
player.vy = 0;
player.rotation = 0;
collectibles = [];
obstacles = [];
particles = [];
currentScoreEl.textContent = '0';
startOverlay.classList.remove('active');
gameOverOverlay.classList.remove('active');
initWallPatterns();
}
function gameOver() {
gameState = 'gameover';
createCrashParticles(player.x + 25, player.y + 25);
if (score > bestScore) {
bestScore = score;
localStorage.setItem('dw_bestScore', bestScore);
bestScoreEl.textContent = bestScore;
}
finalScoreEl.textContent = score;
finalBestEl.textContent = 'Best: ' + bestScore;
setTimeout(() => {
gameOverOverlay.classList.add('active');
}, 500);
}
function restartGame() {
startGame();
}
// Initialize
initWallPatterns();
loop();
</script>
</body>
</html>