Spaces:
Running
Running
Download render.js from atasoglu/degnekci-simulation: direct link, hf CLI and curl.
- Browser
- Download file 11.9 kB
-
https://huggingface.co/spaces/atasoglu/degnekci-simulation/resolve/main/render.js
- Command line
-
hf download hf://spaces/atasoglu/degnekci-simulation/render.js
-
curl -L -o render.js https://huggingface.co/spaces/atasoglu/degnekci-simulation/resolve/main/render.js
11.9 kB
| // Canvas çizimi (2.5D, art.js'teki istiflenmiş katmanlarla). Durağan sahne seviye başına bir kez | |
| // ekran dışı canvas'lara çizilir: bg oyuncunun arkasında kalanlar (üst sıra), fg önünde kalanlar | |
| // (alttaki tır, iki sıralı dünyada alt sıradaki arabalar ve alt kaldırım süsleri). | |
| function mulberry(a) { | |
| return () => { | |
| a |= 0; a = (a + 0x6d2b79f5) | 0; | |
| let t = Math.imul(a ^ (a >>> 15), 1 | a); | |
| t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; | |
| return ((t ^ (t >>> 14)) >>> 0) / 4294967296; | |
| }; | |
| } | |
| const CONFETTI = ["#ffc93c", "#5fd3a5", "#e2483d", "#2f6fb2", "#f28fb0", "#fff6d6"]; | |
| const DEGNEKCI_Y = 56; | |
| class Renderer { | |
| constructor(canvas) { | |
| this.cv = canvas; | |
| this.ctx = canvas.getContext("2d"); | |
| this.bg = document.createElement("canvas"); | |
| this.fg = document.createElement("canvas"); | |
| this.scale = 1; | |
| this.sim = null; | |
| this.systemReduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; | |
| this.reduce = this.systemReduce; // ayarlardan da kapatılabilir | |
| this.fxState = { shake: -1, flash: -1, park: -1 }; | |
| this.particles = []; | |
| this.bubble = null; | |
| this.gesture = null; // değnekçinin son komuta göre hareketi | |
| this.dust = []; | |
| this.stickHit = false; | |
| this.last = performance.now(); | |
| } | |
| // Canvas'ı kutuya en-boy oranını koruyarak sığdırır | |
| fit(box, world) { | |
| const s = Math.min(box.width / world.W, box.height / world.H); | |
| const dpr = Math.min(2, window.devicePixelRatio || 1); | |
| this.cv.style.width = `${Math.floor(world.W * s)}px`; | |
| this.cv.style.height = `${Math.floor(world.H * s)}px`; | |
| this.cv.width = Math.floor(world.W * s * dpr); | |
| this.cv.height = Math.floor(world.H * s * dpr); | |
| const scale = s * dpr; | |
| if (scale !== this.scale) { | |
| this.scale = scale; | |
| if (this.sim) this.bake(); // ölçek değişince keskin kalsın | |
| } | |
| } | |
| setLevel(sim) { | |
| this.sim = sim; | |
| this.seed = Math.floor(Math.random() * 1e9); | |
| this.particles = []; | |
| this.bubble = null; | |
| this.fxState = { shake: -1, flash: -1, park: -1 }; | |
| this.bake(); | |
| } | |
| targetX() { | |
| const s = this.sim.spots[this.sim.target]; | |
| return s.x + s.w / 2; | |
| } | |
| // hedef alt sıradaysa değnekçi alt kaldırımda durur | |
| bottomTarget() { | |
| return this.sim.spots[this.sim.target].row === "bottom"; | |
| } | |
| degnekciY() { | |
| return this.bottomTarget() ? this.sim.bottomCurb + 22 : DEGNEKCI_Y; | |
| } | |
| bake() { | |
| const sim = this.sim, { W, H, CURB } = sim.world, rng = mulberry(this.seed); | |
| for (const c of [this.bg, this.fg]) { | |
| c.width = Math.ceil(W * this.scale); | |
| c.height = Math.ceil(H * this.scale); | |
| } | |
| const g = this.bg.getContext("2d"); | |
| g.setTransform(this.scale, 0, 0, this.scale, 0, 0); | |
| drawAsphalt(g, 0, 0, W, H, rng); | |
| drawCobbles(g, 0, 0, W, CURB, rng); | |
| drawCurbEdge(g, 0, CURB, W); | |
| const bc = sim.bottomCurb, two = sim.world.rows === 2; | |
| if (two) { | |
| // alt kaldırım: yüzü yoldan öte baktığı için kenarı yalnız açık bir şerit olarak görünür | |
| drawCobbles(g, 0, bc - 4, W, H - bc + 4, rng); | |
| g.fillStyle = "#e2d6bf"; | |
| g.fillRect(0, bc - 5, W, 3); | |
| } | |
| const t = sim.spots[sim.target]; | |
| g.fillStyle = "rgba(95, 211, 165, 0.16)"; | |
| g.beginPath(); | |
| g.roundRect(t.x + 5, t.y + 8, t.w - 10, t.h - 14, 10); | |
| g.fill(); | |
| g.strokeStyle = PAL.target; | |
| g.lineWidth = 3; | |
| g.setLineDash([10, 7]); | |
| g.stroke(); | |
| g.setLineDash([]); | |
| g.fillStyle = PAL.line; | |
| const last = sim.spots[sim.spots.length - 1]; | |
| if (sim.level.kind === "paralel") { | |
| // kaldırım boyu cepler: sınırlarda kısa T işaretleri, dış kenarda çizgi | |
| for (const s of [...sim.spots, { x: last.x + last.w }]) rrect(g, s.x - 2, CURB + 4, 4, 14, 2); | |
| rrect(g, sim.spots[0].x, CURB + sim.level.spotD - 2, last.x + last.w - sim.spots[0].x, 4, 2); | |
| } else { | |
| // çizgiler kaldırımdan başlar: üst sırada yukarıdan, alt sırada aşağıdan | |
| for (const row of ["top", "bottom"]) { | |
| const rs = sim.spots.filter((s) => s.row === row), off = row === "top" ? 4 : 0; | |
| for (const s of [...rs, ...rs.slice(-1).map((s) => ({ ...s, x: s.x + s.w }))]) rrect(g, s.x - 2, s.y + off, 4, s.h - 4, 2); | |
| } | |
| } | |
| if (sim.level.kind === "geri") { | |
| // cebe boyalı ok: burun yola baksın (alt sırada ters) | |
| g.save(); | |
| g.translate(t.x + t.w / 2, t.y + t.h / 2); | |
| if (t.row === "bottom") g.scale(1, -1); | |
| g.fillStyle = "rgba(95, 211, 165, 0.55)"; | |
| g.beginPath(); | |
| g.moveTo(0, 26); | |
| g.lineTo(18, 6); | |
| g.lineTo(7, 6); | |
| g.lineTo(7, -24); | |
| g.lineTo(-7, -24); | |
| g.lineTo(-7, 6); | |
| g.lineTo(-18, 6); | |
| g.closePath(); | |
| g.fill(); | |
| g.restore(); | |
| } | |
| // kaldırım süsleri; değnekçinin durduğu yer boş kalır | |
| const tx = this.targetX(), props = [drawTree, drawCayOcagi, drawCat, drawSimitci, drawTree, drawCat]; | |
| let k = Math.floor(rng() * props.length); | |
| const sidewalk = (c, y0, skip) => { | |
| for (let x = 40 + rng() * 30; x < W - 30; x += 95 + rng() * 50) { | |
| if (skip && Math.abs(x - tx) < 75) continue; | |
| const fn = props[k++ % props.length]; | |
| if (fn === drawCat) drawCat(c, x, y0 + 40 + rng() * 10, rng() < 0.5 ? "#f0a35a" : "#d9d4c7"); | |
| else if (fn === drawTree) drawTree(c, x, y0 + 44); | |
| else fn(c, x, y0 + 50); | |
| } | |
| }; | |
| sidewalk(g, 0, !this.bottomTarget()); | |
| const f = this.fg.getContext("2d"); | |
| f.setTransform(this.scale, 0, 0, this.scale, 0, 0); | |
| const rowEnd = CURB + sim.level.spotD + 30; | |
| for (const v of [...sim.parked].sort((a, b) => a.y - b.y)) drawVehicle(v.y < rowEnd ? g : f, v); | |
| if (two) sidewalk(f, bc, this.bottomTarget()); // alt kaldırım süsleri alt sıradaki arabaların önünde | |
| } | |
| // Değnekçinin balonu oyuncunun komutunu tekrarlar, eli de komuta göre hareket eder | |
| say(text, cmd) { | |
| const now = performance.now(); | |
| this.bubble = { text, t: now }; | |
| if (cmd) this.gesture = { cmd, t: now / 1000 }; | |
| } | |
| // Değnekçinin duruşu: varsayılan hâli değneği yere vura vura "gel gel" demek | |
| degnekciPose(t, parking, car) { | |
| const tap = (period, t0 = 0) => { | |
| const p = (((t - t0) / period) % 1 + 1) % 1, up = 0.15, hit = 2.6; | |
| if (p < 0.3) return up + (hit - up) * (p / 0.3) ** 2; // yerçekimiyle hızlanarak iner | |
| if (p < 0.42) return hit; // yerde bir an kalır | |
| const q = (p - 0.42) / 0.58; | |
| return hit - (hit - up) * (1 - (1 - q) ** 2); // yavaşça kalkar | |
| }; | |
| if (parking) return { phi: 0.5 + 0.5 * Math.sin(t * 12), side: 1, other: "wave", hop: Math.abs(Math.sin(t * 14)) * 5, t }; | |
| const gs = this.gesture && t - this.gesture.t < 2.2 ? this.gesture : null; | |
| const cmd = gs ? gs.cmd : "", t0 = gs ? gs.t : 0; | |
| if (cmd === "dur") return { phi: 0.05, side: 1, other: "palm", t }; | |
| if (cmd === "sola" || cmd === "sağa" || cmd === "kır") { | |
| const side = cmd === "sola" ? -1 : cmd === "sağa" ? 1 : car.lastDir; | |
| return { phi: 1.3 + 0.4 * Math.sin((t - t0) * 9), side, t }; // o yöne savurur | |
| } | |
| return { phi: tap(cmd === "geri" ? 0.55 : 0.9, t0), side: 1, t }; | |
| } | |
| fx(type) { | |
| const now = performance.now(), c = this.sim.car; | |
| if (type === "crash") { | |
| this.fxState.shake = now; | |
| this.fxState.flash = now; | |
| } else if (type === "park") { | |
| this.fxState.park = now; | |
| for (let i = 0; i < (this.reduce ? 0 : 46); i++) { | |
| const a = Math.random() * Math.PI * 2, sp = 40 + Math.random() * 90; | |
| this.particles.push({ | |
| x: c.x, y: c.y, z: 20, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp * 0.7, vz: 120 + Math.random() * 140, | |
| rot: Math.random() * 6, vr: (Math.random() - 0.5) * 14, col: CONFETTI[i % CONFETTI.length], life: 1.4, | |
| }); | |
| } | |
| } | |
| } | |
| // değnekçi ve değneğin kaldırdığı toz | |
| drawMan(g, t, dt, parking) { | |
| const dx = this.targetX(), dy = this.degnekciY(), pose = drawDegnekci(g, dx, dy, this.degnekciPose(t, parking, this.sim.car)); | |
| if (pose.hit && !this.stickHit) { | |
| // değnek yere vurdu: küçük toz bulutu | |
| for (let i = 0; i < (this.reduce ? 2 : 5); i++) { | |
| const a = Math.random() * Math.PI * 2; | |
| this.dust.push({ x: dx + pose.tip.x, y: dy + pose.tip.y, vx: Math.cos(a) * 14, vy: Math.sin(a) * 6 - 4, life: 0.4 }); | |
| } | |
| } | |
| this.stickHit = pose.hit; | |
| this.dust = this.dust.filter((p) => (p.life -= dt) > 0); | |
| for (const p of this.dust) { | |
| p.x += p.vx * dt; | |
| p.y += p.vy * dt; | |
| g.fillStyle = `rgba(226, 214, 191, ${p.life * 1.6})`; | |
| disc(g, p.x, p.y, 2 + (0.4 - p.life) * 6); | |
| } | |
| } | |
| draw(sim) { | |
| const now = performance.now(), dt = Math.min(0.05, (now - this.last) / 1000), t = now / 1000; | |
| this.last = now; | |
| const g = this.ctx, { W, H } = sim.world, fx = this.fxState; | |
| let ox = 0, oy = 0; | |
| const sh = (now - fx.shake) / 180; | |
| if (fx.shake >= 0 && sh < 1 && !this.reduce) { | |
| const a = 5 * (1 - sh); | |
| ox = (Math.random() - 0.5) * 2 * a; | |
| oy = (Math.random() - 0.5) * 2 * a; | |
| } | |
| g.setTransform(1, 0, 0, 1, 0, 0); | |
| g.fillStyle = PAL.asphalt; | |
| g.fillRect(0, 0, this.cv.width, this.cv.height); | |
| g.drawImage(this.bg, ox * this.scale, oy * this.scale); | |
| g.setTransform(this.scale, 0, 0, this.scale, ox * this.scale, oy * this.scale); | |
| // hedef cep nefes alır | |
| const s = sim.spots[sim.target]; | |
| g.fillStyle = `rgba(95, 211, 165, ${0.1 + 0.1 * Math.sin(t * 4)})`; | |
| g.beginPath(); | |
| g.roundRect(s.x + 5, s.y + 8, s.w - 10, s.h - 14, 10); | |
| g.fill(); | |
| const pt = (now - fx.park) / 1000, parking = fx.park >= 0 && pt < 1.6; | |
| // değnekçi üst kaldırımda araçların arkasında, alt kaldırımda önünde kalır | |
| const front = this.bottomTarget(); | |
| if (!front) this.drawMan(g, t, dt, parking); | |
| // yoldaki araçlar (oyuncu + trafik) y'ye göre sıralanır: aşağıdaki öndedir | |
| const c = sim.car; | |
| const squash = parking && !this.reduce && pt < 0.8 ? 1 + 0.35 * Math.sin(pt * Math.PI * 5) * Math.exp(-pt * 4) : 1; | |
| for (const v of [c, ...sim.traffic].sort((a, b) => a.y - b.y)) { | |
| if (v !== c) { | |
| drawVehicle(g, v, { brake: v.braking }); | |
| continue; | |
| } | |
| // oyuncu aracı: altında hafif bir hale, park edince zıplama | |
| g.save(); | |
| g.translate(c.x, c.y); | |
| g.rotate(c.ang); | |
| g.strokeStyle = `rgba(255, 236, 160, ${0.35 + 0.15 * Math.sin(t * 5)})`; | |
| g.lineWidth = 3; | |
| g.beginPath(); | |
| g.roundRect(-c.L / 2 - 6, -c.W / 2 - 6, c.L + 12, c.W + 12, 14); | |
| g.stroke(); | |
| g.restore(); | |
| drawVehicle(g, c, { steer: c.steer, brake: c.stopped, reverse: c.gear < 0 && !c.stopped, squash }); | |
| } | |
| g.setTransform(1, 0, 0, 1, 0, 0); | |
| g.drawImage(this.fg, ox * this.scale, oy * this.scale); | |
| g.setTransform(this.scale, 0, 0, this.scale, ox * this.scale, oy * this.scale); | |
| if (front) this.drawMan(g, t, dt, parking); | |
| // konfeti | |
| this.particles = this.particles.filter((p) => (p.life -= dt) > 0); | |
| for (const p of this.particles) { | |
| p.vz -= 420 * dt; | |
| p.x += p.vx * dt; | |
| p.y += p.vy * dt; | |
| p.z = Math.max(0, p.z + p.vz * dt); | |
| if (p.z === 0) p.vx = p.vy = p.vr = 0; | |
| p.rot += p.vr * dt; | |
| g.save(); | |
| g.globalAlpha = Math.min(1, p.life * 2); | |
| g.translate(p.x, p.y - p.z); | |
| g.rotate(p.rot); | |
| g.fillStyle = p.col; | |
| g.fillRect(-3, -2, 6, 4); | |
| g.restore(); | |
| } | |
| if (this.bubble) { | |
| const age = (now - this.bubble.t) / 1000; | |
| if (age > 1.8) this.bubble = null; | |
| else drawBubble(g, this.targetX() + 6, this.degnekciY() - 40, this.bubble.text, W, Math.min(1, (1.8 - age) / 0.3)); | |
| } | |
| const fl = (now - fx.flash) / 260; | |
| if (fx.flash >= 0 && fl < 1) { | |
| g.fillStyle = `rgba(226, 72, 61, ${0.22 * (1 - fl)})`; | |
| g.fillRect(-10, -10, W + 20, H + 20); | |
| } | |
| } | |
| } | |