|
| 1 | +<!DOCTYPE html> |
| 2 | +<html> |
| 3 | +<head> |
| 4 | +<meta charset="utf-8"> |
| 5 | +<meta name="viewport" content="width=device-width, initial-scale=1"> |
| 6 | +<script src="./support.js"></script> |
| 7 | +</head> |
| 8 | +<body> |
| 9 | +<x-dc> |
| 10 | +<helmet> |
| 11 | + <link rel="preconnect" href="https://fonts.googleapis.com"> |
| 12 | + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| 13 | + <link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&display=swap" rel="stylesheet"> |
| 14 | +</helmet> |
| 15 | +<div style="position:relative; width:100%; height:100%; overflow:hidden; font-family:'Chakra Petch',sans-serif; background:radial-gradient(125% 95% at 50% 0%, #161e26 0%, #0b1015 55%, #06090d 100%);"> |
| 16 | + |
| 17 | + <!-- gameplay placeholder texture --> |
| 18 | + <div style="position:absolute; inset:0; opacity:0.045; background-image:repeating-linear-gradient(135deg,#ffffff 0 1px, transparent 1px 16px);"></div> |
| 19 | + <div style="position:absolute; top:14px; left:16px; font:11px ui-monospace,monospace; letter-spacing:0.12em; color:rgba(255,255,255,0.26);">// LIVE GAMEPLAY RENDERS BEHIND HUD</div> |
| 20 | + |
| 21 | + <!-- minimap (bottom-left, canonical) --> |
| 22 | + <sc-if value="{{ showMinimap }}" hint-placeholder-val="{{ true }}"> |
| 23 | + <div style="position:absolute; left:22px; bottom:22px; width:128px; height:128px; border-radius:7px; overflow:hidden; background:#0c1219; border:1px solid rgba(70,224,255,0.18); box-shadow:0 10px 28px rgba(0,0,0,0.45);"> |
| 24 | + <div style="position:absolute; inset:0; opacity:0.55; background-image:repeating-linear-gradient(0deg,#101a22 0 1px, transparent 1px 21px), repeating-linear-gradient(90deg,#101a22 0 1px, transparent 1px 21px);"></div> |
| 25 | + <div style="position:absolute; left:-20px; top:30%; width:180px; height:18px; background:rgba(70,224,255,0.07); transform:rotate(-24deg);"></div> |
| 26 | + <div style="position:absolute; left:50%; top:50%; transform:translate(-50%,-50%) rotate( calc({{headingDeg}} * 1deg) ); width:0; height:0; border-left:7px solid transparent; border-right:7px solid transparent; border-bottom:15px solid {{accent}}; filter:drop-shadow(0 0 6px rgba(70,224,255,0.6));"></div> |
| 27 | + <div style="position:absolute; top:6px; left:50%; transform:translateX(-50%); font:600 9px 'Chakra Petch'; color:rgba(255,255,255,0.55);">N</div> |
| 28 | + <div style="position:absolute; bottom:6px; right:8px; font:500 8px ui-monospace,monospace; letter-spacing:0.14em; color:rgba(255,255,255,0.3);">MAP</div> |
| 29 | + </div> |
| 30 | + </sc-if> |
| 31 | + |
| 32 | + <!-- center cluster --> |
| 33 | + <div style="position:absolute; left:50%; bottom:26px; transform:translateX(-50%); display:flex; flex-direction:column; align-items:center; gap:16px;"> |
| 34 | + |
| 35 | + <!-- compass strip --> |
| 36 | + <div style="position:relative; width:236px; height:34px; overflow:hidden;"> |
| 37 | + <div style="position:absolute; left:0; top:0; bottom:0; width:100%; transform:translateX( calc({{compassX}} * 1px) );"> |
| 38 | + <sc-for list="{{ ticks }}" as="tk" hint-placeholder-count="20"> |
| 39 | + <div style="position:absolute; left:{{tk.left}}px; bottom:0; transform:translateX(-50%);"> |
| 40 | + <div style="position:absolute; bottom:0; left:50%; transform:translateX(-50%); width:1px; height:{{tk.h}}px; background:{{tk.col}};"></div> |
| 41 | + <div style="position:absolute; bottom:16px; left:50%; transform:translateX(-50%); font:600 10px 'Chakra Petch'; color:{{tk.col}}; white-space:nowrap;">{{tk.label}}</div> |
| 42 | + </div> |
| 43 | + </sc-for> |
| 44 | + </div> |
| 45 | + <div style="position:absolute; left:118px; top:6px; bottom:0; width:1px; background:{{accent}}; opacity:0.7;"></div> |
| 46 | + <div style="position:absolute; left:118px; top:0; transform:translateX(-50%); width:0; height:0; border-left:5px solid transparent; border-right:5px solid transparent; border-top:6px solid {{accent}};"></div> |
| 47 | + <div style="position:absolute; inset:0; pointer-events:none; background:linear-gradient(90deg, rgba(8,11,15,0.96), transparent 16%, transparent 84%, rgba(8,11,15,0.96));"></div> |
| 48 | + </div> |
| 49 | + |
| 50 | + <!-- gauges row --> |
| 51 | + <div style="display:flex; align-items:center; gap:24px;"> |
| 52 | + |
| 53 | + <!-- left rail: fuel + engine --> |
| 54 | + <div style="display:flex; align-items:flex-end; gap:16px;"> |
| 55 | + <div style="display:flex; flex-direction:column; align-items:center; gap:7px;"> |
| 56 | + <div style="font:600 10px 'Chakra Petch'; color:#fff;">{{fuel}}</div> |
| 57 | + <div style="position:relative; width:7px; height:78px; border-radius:5px; background:rgba(255,255,255,0.08); overflow:hidden;"> |
| 58 | + <div style="position:absolute; left:0; right:0; bottom:0; height:calc({{fuel}} * 1%); background:{{fuelCol}}; border-radius:5px;"></div> |
| 59 | + </div> |
| 60 | + <div style="font:500 9px 'Chakra Petch'; letter-spacing:0.16em; color:rgba(255,255,255,0.45);">FUEL</div> |
| 61 | + </div> |
| 62 | + <div style="display:flex; flex-direction:column; align-items:center; gap:7px;"> |
| 63 | + <div style="font:600 10px 'Chakra Petch'; color:#fff;">{{eng}}</div> |
| 64 | + <div style="position:relative; width:7px; height:78px; border-radius:5px; background:rgba(255,255,255,0.08); overflow:hidden;"> |
| 65 | + <div style="position:absolute; left:0; right:0; bottom:0; height:calc({{eng}} * 1%); background:{{engCol}}; border-radius:5px;"></div> |
| 66 | + </div> |
| 67 | + <div style="font:500 9px 'Chakra Petch'; letter-spacing:0.16em; color:rgba(255,255,255,0.45);">ENG</div> |
| 68 | + </div> |
| 69 | + </div> |
| 70 | + |
| 71 | + <!-- speedo slot --> |
| 72 | + <div style="min-width:300px; display:flex; justify-content:center;"> |
| 73 | + |
| 74 | + <!-- RING --> |
| 75 | + <sc-if value="{{ isRing }}" hint-placeholder-val="{{ true }}"> |
| 76 | + <div style="position:relative; width:206px; height:206px;"> |
| 77 | + <div style="position:absolute; inset:0; border-radius:50%; background:conic-gradient({{accent}} calc({{pSpeed}} * 1%), rgba(255,255,255,0.07) 0); -webkit-mask:radial-gradient(circle, transparent 80px, #000 81px); mask:radial-gradient(circle, transparent 80px, #000 81px);"></div> |
| 78 | + <div style="position:absolute; inset:16px; border-radius:50%; border:1px solid rgba(255,255,255,0.05);"></div> |
| 79 | + <div style="position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;"> |
| 80 | + <div style="font:600 60px 'Chakra Petch'; color:#fff; line-height:0.85; text-shadow:0 0 22px rgba(70,224,255,0.32);">{{speedInt}}</div> |
| 81 | + <div style="font:500 10px 'Chakra Petch'; letter-spacing:0.32em; color:rgba(255,255,255,0.5); margin-top:7px;">KM/H</div> |
| 82 | + </div> |
| 83 | + </div> |
| 84 | + </sc-if> |
| 85 | + |
| 86 | + <!-- ARC --> |
| 87 | + <sc-if value="{{ isArc }}" hint-placeholder-val="{{ true }}"> |
| 88 | + <div style="display:flex; flex-direction:column; align-items:center;"> |
| 89 | + <div style="position:relative; width:280px; height:140px; overflow:hidden;"> |
| 90 | + <div style="position:absolute; top:0; left:0; width:280px; height:280px; border-radius:50%; background:conic-gradient(from -90deg, {{accent}} calc({{pArc}} * 1%), rgba(255,255,255,0.07) 0); -webkit-mask:radial-gradient(circle, transparent 108px, #000 109px); mask:radial-gradient(circle, transparent 108px, #000 109px);"></div> |
| 91 | + </div> |
| 92 | + <div style="display:flex; flex-direction:column; align-items:center; margin-top:-50px;"> |
| 93 | + <div style="font:600 56px 'Chakra Petch'; color:#fff; line-height:0.85; text-shadow:0 0 22px rgba(70,224,255,0.32);">{{speedInt}}</div> |
| 94 | + <div style="font:500 10px 'Chakra Petch'; letter-spacing:0.32em; color:rgba(255,255,255,0.5); margin-top:5px;">KM/H</div> |
| 95 | + </div> |
| 96 | + </div> |
| 97 | + </sc-if> |
| 98 | + |
| 99 | + <!-- LINEAR --> |
| 100 | + <sc-if value="{{ isLinear }}" hint-placeholder-val="{{ true }}"> |
| 101 | + <div style="display:flex; flex-direction:column; align-items:flex-start; gap:11px; width:280px;"> |
| 102 | + <div style="display:flex; align-items:baseline; gap:11px;"> |
| 103 | + <div style="font:600 72px 'Chakra Petch'; color:#fff; line-height:0.78; text-shadow:0 0 22px rgba(70,224,255,0.32);">{{speedInt}}</div> |
| 104 | + <div style="font:500 11px 'Chakra Petch'; letter-spacing:0.3em; color:rgba(255,255,255,0.5);">KM/H</div> |
| 105 | + </div> |
| 106 | + <div style="position:relative; width:100%; height:6px; border-radius:4px; background:rgba(255,255,255,0.08); overflow:hidden;"> |
| 107 | + <div style="position:absolute; left:0; top:0; bottom:0; border-radius:4px; width:calc({{speedPct}} * 1%); background:{{accent}}; box-shadow:0 0 12px {{accent}};"></div> |
| 108 | + <div style="position:absolute; inset:0; background-image:repeating-linear-gradient(90deg, rgba(8,11,15,0.85) 0 1px, transparent 1px 26px);"></div> |
| 109 | + </div> |
| 110 | + </div> |
| 111 | + </sc-if> |
| 112 | + |
| 113 | + </div> |
| 114 | + |
| 115 | + <!-- right rail: turbo + belt --> |
| 116 | + <div style="display:flex; flex-direction:column; align-items:center; gap:12px;"> |
| 117 | + <div style="display:flex; flex-direction:column; align-items:center; gap:7px;"> |
| 118 | + <div style="font:600 10px 'Chakra Petch'; color:#fff;">{{boost}}</div> |
| 119 | + <div style="position:relative; width:7px; height:60px; border-radius:5px; background:rgba(255,255,255,0.08); overflow:hidden;"> |
| 120 | + <div style="position:absolute; left:0; right:0; bottom:0; height:calc({{boost}} * 1%); background:{{accent}}; border-radius:5px; box-shadow:0 0 8px {{accent}};"></div> |
| 121 | + </div> |
| 122 | + <div style="font:500 9px 'Chakra Petch'; letter-spacing:0.16em; color:rgba(255,255,255,0.45);">TURBO</div> |
| 123 | + </div> |
| 124 | + <div style="display:flex; align-items:center; gap:6px; padding:5px 9px; border-radius:20px; background:rgba(70,224,255,0.1); border:1px solid rgba(70,224,255,0.32);"> |
| 125 | + <div style="width:6px; height:6px; border-radius:50%; background:{{accent}}; box-shadow:0 0 8px {{accent}};"></div> |
| 126 | + <div style="font:600 9px 'Chakra Petch'; letter-spacing:0.16em; color:{{accent}};">BELT</div> |
| 127 | + </div> |
| 128 | + </div> |
| 129 | + |
| 130 | + </div> |
| 131 | + |
| 132 | + <!-- tach line --> |
| 133 | + <div style="display:flex; align-items:flex-end; gap:16px;"> |
| 134 | + <div style="display:flex; flex-direction:column; align-items:center; line-height:1;"> |
| 135 | + <div style="font:700 22px 'Chakra Petch'; color:#fff;">{{gear}}</div> |
| 136 | + <div style="font:500 8px 'Chakra Petch'; letter-spacing:0.2em; color:rgba(255,255,255,0.4); margin-top:3px;">GEAR</div> |
| 137 | + </div> |
| 138 | + <div style="display:flex; align-items:flex-end; gap:3px; height:28px;"> |
| 139 | + <sc-for list="{{ seg }}" as="s" hint-placeholder-count="10"> |
| 140 | + <div style="width:5px; border-radius:2px; height:{{s.h}}px; background:{{s.bg}};"></div> |
| 141 | + </sc-for> |
| 142 | + </div> |
| 143 | + <div style="display:flex; flex-direction:column; align-items:flex-start; line-height:1;"> |
| 144 | + <div style="font:600 16px 'Chakra Petch'; color:#fff;">{{rpmInt}}</div> |
| 145 | + <div style="font:500 8px 'Chakra Petch'; letter-spacing:0.2em; color:rgba(255,255,255,0.4); margin-top:3px;">RPM</div> |
| 146 | + </div> |
| 147 | + </div> |
| 148 | + |
| 149 | + </div> |
| 150 | +</div> |
| 151 | +</x-dc> |
| 152 | +<script type="text/x-dc" data-dc-script data-props="{"$preview":{"width":880,"height":495},"variant":{"editor":"enum","options":["ring","arc","linear"],"default":"ring","tsType":"string"},"accent":{"editor":"color","default":"#46e0ff","tsType":"string"},"showMinimap":{"editor":"boolean","default":true,"tsType":"boolean"},"animate":{"editor":"boolean","default":true,"tsType":"boolean"}}"> |
| 153 | +class Component extends DCLogic { |
| 154 | + state = { now: 0 }; |
| 155 | + componentDidMount() { |
| 156 | + if (this.props.animate === false) { this.setState({ now: 3400 }); return; } |
| 157 | + this._start = performance.now(); |
| 158 | + this._last = -999; |
| 159 | + const loop = () => { |
| 160 | + this._raf = requestAnimationFrame(loop); |
| 161 | + const el = performance.now() - this._start; |
| 162 | + if (el - this._last < 32) return; |
| 163 | + this._last = el; |
| 164 | + this.setState({ now: el }); |
| 165 | + }; |
| 166 | + this._raf = requestAnimationFrame(loop); |
| 167 | + } |
| 168 | + componentWillUnmount() { if (this._raf) cancelAnimationFrame(this._raf); } |
| 169 | + clamp(v, a, b) { return Math.max(a, Math.min(b, v)); } |
| 170 | + renderVals() { |
| 171 | + const C = (v, a, b) => this.clamp(v, a, b); |
| 172 | + const accent = this.props.accent || '#46e0ff'; |
| 173 | + const variant = this.props.variant || 'ring'; |
| 174 | + const t = (this.state.now || 0) / 1000; |
| 175 | + const MAXS = 200, MAXR = 8000; |
| 176 | + const f = (x) => 90 + 78 * Math.sin(x * 0.22) + 12 * Math.sin(x * 0.9); |
| 177 | + const sp = C(f(t), 0, MAXS); |
| 178 | + const accel = sp - C(f(t - 0.12), 0, MAXS); |
| 179 | + const bands = [[0, 20], [20, 45], [45, 75], [75, 110], [110, 150], [150, 200]]; |
| 180 | + let gear = 1, frac = 0; |
| 181 | + for (let i = 0; i < bands.length; i++) { |
| 182 | + if (sp >= bands[i][0]) { gear = i + 1; frac = (sp - bands[i][0]) / (bands[i][1] - bands[i][0]); } |
| 183 | + } |
| 184 | + frac = C(frac, 0, 1); |
| 185 | + const rpm = C(1200 + frac * 5600 + 60 * Math.sin(t * 7), 700, MAXR); |
| 186 | + const speedFrac = sp / MAXS, rpmFrac = rpm / MAXR; |
| 187 | + const boost = C(accel * 22, 0, 100); |
| 188 | + const fuel = 44 + 28 * Math.cos(t * 0.04); |
| 189 | + const eng = 88 + 6 * Math.sin(t * 0.3); |
| 190 | + const headingDeg = ((t * 9) % 360 + 360) % 360; |
| 191 | + const lit = Math.round(rpmFrac * 10); |
| 192 | + const heights = [9, 11, 13, 15, 17, 19, 21, 23, 25, 27]; |
| 193 | + const seg = heights.map((h, i) => ({ |
| 194 | + h, |
| 195 | + bg: i < lit ? (i >= 8 ? '#ff5a52' : (i >= 7 ? '#ffc24d' : accent)) : 'rgba(255,255,255,0.10)' |
| 196 | + })); |
| 197 | + const barCol = (p) => p > 50 ? accent : (p > 22 ? '#ffc24d' : '#ff5a52'); |
| 198 | + const lab = { 0: 'N', 45: 'NE', 90: 'E', 135: 'SE', 180: 'S', 225: 'SW', 270: 'W', 315: 'NW' }; |
| 199 | + const ticks = []; |
| 200 | + for (let d = 0; d <= 720; d += 15) { |
| 201 | + const major = d % 45 === 0; |
| 202 | + ticks.push({ |
| 203 | + left: d * 3, |
| 204 | + h: major ? 12 : 6, |
| 205 | + col: major ? 'rgba(255,255,255,0.72)' : 'rgba(255,255,255,0.20)', |
| 206 | + label: major ? (lab[d % 360] || '') : '' |
| 207 | + }); |
| 208 | + } |
| 209 | + return { |
| 210 | + accent, |
| 211 | + isRing: variant === 'ring', isArc: variant === 'arc', isLinear: variant === 'linear', |
| 212 | + speedInt: Math.round(sp), rpmInt: Math.round(rpm), gear, |
| 213 | + pSpeed: speedFrac * 100, pArc: speedFrac * 50, speedPct: speedFrac * 100, |
| 214 | + seg, |
| 215 | + fuel: Math.round(fuel), eng: Math.round(eng), boost: Math.round(boost), |
| 216 | + fuelCol: barCol(fuel), engCol: barCol(eng), |
| 217 | + headingDeg: Math.round(headingDeg), |
| 218 | + compassX: 118 - headingDeg * 3, |
| 219 | + ticks, |
| 220 | + showMinimap: this.props.showMinimap !== false |
| 221 | + }; |
| 222 | + } |
| 223 | +} |
| 224 | +</script> |
| 225 | +</body> |
| 226 | +</html> |
0 commit comments