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