| Server IP : 202.61.199.114 / Your IP : 216.73.217.139 Web Server : nginx/1.22.1 System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64 User : web20 ( 1018) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /var/www/arni-solutions.de/web/slots/js/engine/ |
Upload File : |
/* Reel Royale – WheelEngine: animierter Roulette-Kessel + Kugel.
Rad und Kugel drehen gegenläufig, beschleunigen/laufen aus, Kugel fällt ins
serverbestimmte Fach (top = Zeiger). Reine Canvas-Grafik. */
export class WheelEngine {
constructor(canvas, config, audio) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.wheel = config.wheel; // [0,32,15,...]
this.colors = config.colors; // {n: color}
this.audio = audio;
this.n = this.wheel.length; // 37
this.seg = (Math.PI * 2) / this.n;
this.wheelAngle = 0;
this.ballAngle = 0;
this.ballR = 1;
this._dpr = Math.min(window.devicePixelRatio || 1, 2);
this._setup();
this.draw(0, 0, 0.86);
}
_setup() {
const size = this.canvas.clientWidth || 380;
this.canvas.width = size * this._dpr;
this.canvas.height = size * this._dpr;
this.ctx.setTransform(this._dpr, 0, 0, this._dpr, 0, 0);
this.size = size;
this.cx = size / 2; this.cy = size / 2;
this.R = size / 2 - 4;
}
_segColor(n) {
const c = this.colors[n];
return c === 'green' ? '#0a8a4f' : c === 'red' ? '#d4332f' : '#1a1a20';
}
// canvas-Winkel: top = -PI/2, im Uhrzeigersinn wachsend
draw(wheelAngle, ballAngle, ballR) {
const { ctx, cx, cy, R } = this;
ctx.clearRect(0, 0, this.size, this.size);
// Außenring (Holz)
ctx.save(); ctx.translate(cx, cy);
ctx.beginPath(); ctx.arc(0, 0, R, 0, 7);
const ring = ctx.createRadialGradient(0, 0, R * 0.7, 0, 0, R);
ring.addColorStop(0, '#6b3f1d'); ring.addColorStop(1, '#2c1809');
ctx.fillStyle = ring; ctx.fill();
ctx.lineWidth = 2; ctx.strokeStyle = 'rgba(255,215,140,.5)'; ctx.stroke();
// Fächer (im gedrehten Rad-Frame)
ctx.save();
ctx.rotate(wheelAngle);
const rOuter = R * 0.9, rInner = R * 0.55;
for (let i = 0; i < this.n; i++) {
const a0 = i * this.seg - Math.PI / 2;
const a1 = a0 + this.seg;
ctx.beginPath();
ctx.arc(0, 0, rOuter, a0, a1);
ctx.arc(0, 0, rInner, a1, a0, true);
ctx.closePath();
ctx.fillStyle = this._segColor(this.wheel[i]);
ctx.fill();
ctx.lineWidth = 1; ctx.strokeStyle = 'rgba(255,255,255,.18)'; ctx.stroke();
}
// Nabe
ctx.beginPath(); ctx.arc(0, 0, rInner, 0, 7);
const hub = ctx.createRadialGradient(0, 0, 4, 0, 0, rInner);
hub.addColorStop(0, '#caa92e'); hub.addColorStop(.6, '#8a6b1d'); hub.addColorStop(1, '#3a2c0c');
ctx.fillStyle = hub; ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,.4)'; ctx.stroke();
// Kreuz-Speichen
ctx.strokeStyle = 'rgba(255,240,200,.5)'; ctx.lineWidth = 3;
for (let k = 0; k < 4; k++) {
ctx.beginPath(); ctx.moveTo(0, 0);
ctx.lineTo(Math.cos(k * Math.PI / 2) * rInner, Math.sin(k * Math.PI / 2) * rInner); ctx.stroke();
}
ctx.restore(); // zurück in den (cx,cy)-Frame ohne Rad-Rotation
// Zahlen als eigene Ebene – saubere Einzel-Transformation, garantiert alle 37 sichtbar
ctx.font = `800 ${Math.max(9, Math.round(R * 0.062))}px Inter, Arial, sans-serif`;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.lineJoin = 'round'; ctx.lineWidth = Math.max(2, R * 0.018);
const rNum = rOuter - R * 0.085;
for (let i = 0; i < this.n; i++) {
const phi = wheelAngle + i * this.seg + this.seg / 2; // im Uhrzeigersinn ab oben
const label = String(this.wheel[i]);
ctx.save();
ctx.translate(Math.sin(phi) * rNum, -Math.cos(phi) * rNum);
ctx.rotate(phi); // radial, dreht mit dem Rad (wie echtes Roulette)
ctx.strokeStyle = 'rgba(0,0,0,.7)'; ctx.strokeText(label, 0, 0);
ctx.fillStyle = '#fff'; ctx.fillText(label, 0, 0);
ctx.restore();
}
ctx.restore(); // outer (translate cx,cy)
// Kugel (absolute Lage, top=-PI/2)
const br = R * ballR;
const ba = ballAngle - Math.PI / 2;
const bx = cx + Math.cos(ba) * br;
const by = cy + Math.sin(ba) * br;
ctx.beginPath(); ctx.arc(bx, by, Math.max(4, R * 0.035), 0, 7);
const bg = ctx.createRadialGradient(bx - 2, by - 2, 1, bx, by, R * 0.04);
bg.addColorStop(0, '#fff'); bg.addColorStop(1, '#bcbcbc');
ctx.fillStyle = bg; ctx.fill();
// Zeiger oben
ctx.beginPath(); ctx.moveTo(cx - 8, 2); ctx.lineTo(cx + 8, 2); ctx.lineTo(cx, 20); ctx.closePath();
ctx.fillStyle = '#ffce53'; ctx.fill();
}
/** Dreht zum gegebenen Gewinn-Index (Position in this.wheel). Promise nach Stopp. */
spin(number) {
const idx = this.wheel.indexOf(number);
this.audio.play('rlStart');
// Endwinkel des Rades, sodass Fach idx oben (top) liegt
const targetMod = -(idx * this.seg + this.seg / 2);
const wheelStart = this.wheelAngle % (Math.PI * 2);
const wheelTurns = 5;
let wheelEnd = wheelTurns * Math.PI * 2 + targetMod;
while (wheelEnd < wheelStart + 3 * Math.PI * 2) wheelEnd += Math.PI * 2;
const ballTurns = 9;
const ballStart = 0;
const ballEnd = -ballTurns * Math.PI * 2; // endet bei top (0 mod 2π)
const dur = 5200;
const t0 = performance.now();
let lastTick = -1;
const easeWheel = (t) => 1 - Math.pow(1 - t, 3);
const easeBall = (t) => 1 - Math.pow(1 - t, 4);
return new Promise((resolve) => {
const anim = (now) => {
const t = Math.min(1, (now - t0) / dur);
this.wheelAngle = wheelStart + (wheelEnd - wheelStart) * easeWheel(t);
this.ballAngle = ballStart + (ballEnd - ballStart) * easeBall(t);
// Kugel fällt in den letzten 22% nach innen
if (t < 0.78) this.ballR = 0.86;
else this.ballR = 0.86 - (0.86 - 0.72) * ((t - 0.78) / 0.22);
this.draw(this.wheelAngle, this.ballAngle, this.ballR);
// Tick-Sound abhängig von Geschwindigkeit
const tickN = Math.floor((1 - t) * 60 * (1 - t));
if (tickN !== lastTick && t < 0.92) { lastTick = tickN; if (Math.random() < 0.6) this.audio.play('ballRoll'); }
if (t >= 1) { this.audio.play('ballDrop'); resolve(); return; }
requestAnimationFrame(anim);
};
requestAnimationFrame(anim);
});
}
resize() { this._setup(); this.draw(this.wheelAngle, this.ballAngle, 0.86); }
}