From ebed5a2d424ca720c20c99f3b68e88691c1f93e3 Mon Sep 17 00:00:00 2001 From: sthag Date: Sat, 21 Mar 2026 14:49:10 +0100 Subject: [PATCH] feat: Add new game screen - Add first person view screen - Add HippieCrosshair class - Controls for crosshair options - Add styles to game module --- source/code/game.js | 230 +++++++++++++++++++ source/screens/demo/examples/game/fpv.liquid | 79 +++++++ source/style/modules/_game.scss | 19 ++ 3 files changed, 328 insertions(+) create mode 100644 source/code/game.js create mode 100644 source/screens/demo/examples/game/fpv.liquid diff --git a/source/code/game.js b/source/code/game.js new file mode 100644 index 0000000..1492a63 --- /dev/null +++ b/source/code/game.js @@ -0,0 +1,230 @@ +class HippieCrosshair { + constructor(canvas, options = {}) { + this.canvas = canvas; + this.ctx = canvas.getContext('2d'); + + this.debug = options.debug || false; + + // Crosshair options + this.size = options.size || 16; + this.thickness = options.thickness || 2; + this.color = options.color || '#000'; + this.gapSize = options.gapSize || 8; + this.style = options.style || 'cross'; + + // Line options + this.lineColor = options.lineColor || '#fff'; + this.lineWidth = options.lineWidth || 1; + this.showConnector = options.showConnector !== false; + + // Symbol options + this.symbolDistance = options.symbolDistance || 128; // Distance to draw next symbol + this.symbolSpacing = options.symbolSpacing || 64; // Space between symbols + this.symbolSize = options.symbolSize || 8; + this.symbolColor = options.symbolColor || '#000'; + this.symbolStyle = options.symbolStyle || 'arrow'; + + this.mouseX = canvas.width / 2; + this.mouseY = canvas.height / 2; + + this.setupEventListeners(); + this.animate(); + } + + setupEventListeners() { + document.addEventListener('mousemove', (e) => { + this.mouseX = e.clientX; + this.mouseY = e.clientY; + }); + } + + animate() { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + + if (this.showConnector) { + if (this.debug) this.drawLine(); + this.drawConnector(); + } + + // TODO: Autom. Zug zum Zentrum hin ermöglichen + this.drawCrosshair(); + + requestAnimationFrame(() => this.animate()); + } + + drawLine() { + const centerX = this.canvas.width / 2; + const centerY = this.canvas.height / 2; + + this.ctx.strokeStyle = this.lineColor; + this.ctx.lineWidth = this.lineWidth; + this.ctx.beginPath(); + this.ctx.moveTo(centerX, centerY); + this.ctx.lineTo(this.mouseX, this.mouseY); + this.ctx.stroke(); + } + + // TODO: Ausblenden nach Distanz + drawConnector() { + const centerX = this.canvas.width / 2; + const centerY = this.canvas.height / 2; + + const dx = this.mouseX - centerX; + const dy = this.mouseY - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + + // Only draw symbols if cursor is far enough from center + if (distance < this.symbolDistance) { + return; + } + + const angle = Math.atan2(dy, dx); + + // Calculate how many symbols to draw + const symbolCount = Math.floor((distance - this.symbolDistance) / this.symbolSpacing); + + for (let i = 0; i < symbolCount; i++) { + const symbolDistance = this.symbolDistance + (i * this.symbolSpacing); + const symbolX = centerX + Math.cos(angle) * symbolDistance; + const symbolY = centerY + Math.sin(angle) * symbolDistance; + + this.drawSymbol(symbolX, symbolY, angle); + } + } + + drawSymbol(x, y, angle = 0) { + this.ctx.fillStyle = this.symbolColor; + this.ctx.strokeStyle = this.symbolColor; + this.ctx.lineWidth = 1; + + switch (this.symbolStyle) { + case 'circle': + this.ctx.beginPath(); + this.ctx.arc(x, y, this.symbolSize / 2, 0, Math.PI * 2); + this.ctx.fill(); + break; + case 'diamond': + const size = this.symbolSize - (this.symbolSize / 4); + this.ctx.beginPath(); + this.ctx.moveTo(x, y - size); + this.ctx.lineTo(x + size, y); + this.ctx.lineTo(x, y + size); + this.ctx.lineTo(x - size, y); + this.ctx.closePath(); + this.ctx.fill(); + break; + case 'square': + this.ctx.fillRect( + x - this.symbolSize / 2, + y - this.symbolSize / 2, + this.symbolSize, + this.symbolSize + ); + break; + case 'arrow': + this.arrow(x, y, angle); + break; + } + } + + arrow(x, y, angle) { + const size = this.symbolSize - (this.symbolSize / 4); + + this.ctx.save(); + this.ctx.translate(x, y); + this.ctx.rotate(angle); + + // Arrow pointing right + this.ctx.beginPath(); + this.ctx.moveTo(size, 0); // Tip + this.ctx.lineTo(-size, -size); // Back left + // this.ctx.lineTo(-size * 0.4, 0); // Middle + this.ctx.lineTo(-size, size); // Back right + this.ctx.closePath(); + this.ctx.fill(); + + this.ctx.restore(); + } + + drawCrosshair() { + this.ctx.strokeStyle = this.color; + this.ctx.lineWidth = this.thickness; + this.ctx.lineCap = 'round'; + + switch (this.style) { + case 'cross': + this.cross(); + break; + case 'circle': + this.circle(); + break; + case 'dot': + this.dot(); + break; + } + } + + cross() { + // Horizontal line + this.ctx.beginPath(); + this.ctx.moveTo(this.mouseX - this.size, this.mouseY); + this.ctx.lineTo(this.mouseX - this.gapSize, this.mouseY); + this.ctx.stroke(); + + this.ctx.beginPath(); + this.ctx.moveTo(this.mouseX + this.gapSize, this.mouseY); + this.ctx.lineTo(this.mouseX + this.size, this.mouseY); + this.ctx.stroke(); + + // Vertical line + this.ctx.beginPath(); + this.ctx.moveTo(this.mouseX, this.mouseY - this.size); + this.ctx.lineTo(this.mouseX, this.mouseY - this.gapSize); + this.ctx.stroke(); + + this.ctx.beginPath(); + this.ctx.moveTo(this.mouseX, this.mouseY + this.gapSize); + this.ctx.lineTo(this.mouseX, this.mouseY + this.size); + this.ctx.stroke(); + } + + circle() { + // Outer circle + this.ctx.beginPath(); + this.ctx.arc(this.mouseX, this.mouseY, this.size, 0, Math.PI * 2); + this.ctx.stroke(); + + // Inner dot + this.ctx.fillStyle = this.color; + this.ctx.beginPath(); + this.ctx.arc(this.mouseX, this.mouseY, this.thickness * 1.5, 0, Math.PI * 2); + this.ctx.fill(); + } + + dot() { + this.ctx.fillStyle = this.color; + this.ctx.beginPath(); + this.ctx.arc(this.mouseX, this.mouseY, this.size / 4, 0, Math.PI * 2); + this.ctx.fill(); + } + + setCrosshairStyle(style) { + this.style = style; + } + + setColor(color) { + this.color = color; + } + + setSymbolStyle(style) { + this.symbolStyle = style; + } + + setSymbolColor(color) { + this.symbolColor = color; + } + + setLineVisible(visible) { + this.showConnector = visible; + } +} diff --git a/source/screens/demo/examples/game/fpv.liquid b/source/screens/demo/examples/game/fpv.liquid new file mode 100644 index 0000000..a4f53a7 --- /dev/null +++ b/source/screens/demo/examples/game/fpv.liquid @@ -0,0 +1,79 @@ +--- +title: FPV +tags: +- game +--- +{% assign bodyClass = 'body_fpv' -%} +{% layout 'hippie/simple.liquid' %} + +{% block links %} +{{ block.super -}} + +{% endblock %} + +{% block body %} + + +
+ + + +
+{% endblock %} + +{% block assets %} + +{% endblock %} + +{% block script %} + + +{% endblock %} \ No newline at end of file diff --git a/source/style/modules/_game.scss b/source/style/modules/_game.scss index 6d49400..d9a36f1 100644 --- a/source/style/modules/_game.scss +++ b/source/style/modules/_game.scss @@ -1,3 +1,5 @@ +@use "sass:map"; + @use "../hippie-style/hippie"; .body_game { @@ -24,4 +26,21 @@ } } } +} + +.body_fpv { + @extend .h_full_view; + + canvas { + display: block; + cursor: none; + background-color: lightblue; + } + + .controls { + position: fixed; + top: 0; + left: 0; + border-radius: hippie.$radius_basic; + } } \ No newline at end of file