1106 lines
30 KiB
HTML
1106 lines
30 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>crosshair gen</title>
|
||
<style>
|
||
:root{
|
||
--black:#000;
|
||
--white:#fff;
|
||
--line:#262626;
|
||
--muted:#888;
|
||
--muted-strong:#aaa;
|
||
--control:#101010;
|
||
--control-hover:#181818;
|
||
--bright:rgba(255,255,255,.84);
|
||
--soft:rgba(255,255,255,.65);
|
||
}
|
||
*{box-sizing:border-box}
|
||
html,body{min-height:100%;background:#000}
|
||
body{
|
||
margin:0;
|
||
padding:10px;
|
||
background:#000;
|
||
color:var(--bright);
|
||
font:13px/1.3 "SFMono-Regular",Consolas,"Liberation Mono",monospace
|
||
}
|
||
.app{
|
||
width:min(100%,1080px);
|
||
min-height:calc(100vh - 20px);
|
||
margin:auto;
|
||
display:grid;
|
||
grid-template-columns:272px minmax(0,1fr);
|
||
gap:8px
|
||
}
|
||
.panel{background:#000;border:0px solid var(--line);border-radius:0}
|
||
.sidebar{
|
||
padding:7px;
|
||
display:flex;
|
||
flex-direction:column;
|
||
gap:4px;
|
||
max-height:calc(100vh - 20px);
|
||
overflow:auto;
|
||
scrollbar-width:thin;
|
||
scrollbar-color:#555 #000
|
||
}
|
||
.field{padding:5px 6px;background:#000}
|
||
.label{
|
||
display:flex;
|
||
align-items:center;
|
||
justify-content:space-between;
|
||
gap:6px;
|
||
margin:0 0 4px;
|
||
color:var(--muted);
|
||
font-size:10px;
|
||
letter-spacing:.07em;
|
||
text-transform:lowercase
|
||
}
|
||
.value{color:var(--bright);letter-spacing:0}
|
||
select,input[type=number],textarea,button{font:inherit}
|
||
select,input[type=number]{
|
||
width:100%;
|
||
height:29px;
|
||
padding:0 7px;
|
||
color:var(--bright);
|
||
background:var(--control);
|
||
border:0;
|
||
border-radius:0;
|
||
outline:none;
|
||
font-size:11px
|
||
}
|
||
select:focus,input[type=number]:focus,textarea:focus,.map-button:focus{
|
||
box-shadow:inset 0 0 0 1px rgba(255,255,255,.55)
|
||
}
|
||
.range-row{
|
||
display:grid;
|
||
grid-template-columns:minmax(0,1fr) 60px;
|
||
gap:7px;
|
||
align-items:center
|
||
}
|
||
input[type=range]{
|
||
appearance:none;
|
||
width:100%;
|
||
height:2px;
|
||
margin:0;
|
||
background:#484848;
|
||
border-radius:999px;
|
||
outline:none;
|
||
cursor:pointer
|
||
}
|
||
input[type=range]::-webkit-slider-thumb{
|
||
appearance:none;
|
||
width:12px;
|
||
height:12px;
|
||
background:rgba(255,255,255,.82);
|
||
border:0;
|
||
border-radius:50%
|
||
}
|
||
input[type=range]::-moz-range-thumb{
|
||
width:12px;
|
||
height:12px;
|
||
background:rgba(255,255,255,.82);
|
||
border:0;
|
||
border-radius:50%
|
||
}
|
||
.number-control{
|
||
height:28px;
|
||
display:grid;
|
||
grid-template-columns:19px minmax(0,1fr) 19px;
|
||
background:var(--control);
|
||
border-radius:999px;
|
||
overflow:hidden
|
||
}
|
||
.number-control input{
|
||
min-width:0;
|
||
height:28px;
|
||
padding:0;
|
||
text-align:center;
|
||
color:var(--bright);
|
||
background:transparent;
|
||
border:0!important;
|
||
border-radius:0!important;
|
||
appearance:textfield;
|
||
box-shadow:none!important;
|
||
font-size:11px
|
||
}
|
||
.number-control input::-webkit-inner-spin-button,
|
||
.number-control input::-webkit-outer-spin-button{appearance:none;margin:0}
|
||
.step{
|
||
padding:0;
|
||
border:0;
|
||
border-radius:0;
|
||
background:transparent;
|
||
color:var(--soft);
|
||
cursor:pointer;
|
||
font-size:15px;
|
||
line-height:1
|
||
}
|
||
.step:hover{color:#000;background:rgba(255,255,255,.82)}
|
||
.toggle-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px}
|
||
.toggle{
|
||
min-height:29px;
|
||
padding:5px 6px;
|
||
display:flex;
|
||
align-items:center;
|
||
justify-content:space-between;
|
||
gap:5px;
|
||
background:var(--control);
|
||
color:var(--soft);
|
||
cursor:pointer;
|
||
font-size:10px;
|
||
border:0;
|
||
border-radius:0;
|
||
text-transform:lowercase
|
||
}
|
||
.toggle:hover{color:var(--bright);background:var(--control-hover)}
|
||
.toggle input{
|
||
appearance:none;
|
||
width:24px;
|
||
height:14px;
|
||
margin:0;
|
||
position:relative;
|
||
border:0;
|
||
border-radius:999px;
|
||
background:#3a3a3a;
|
||
cursor:pointer
|
||
}
|
||
.toggle input:after{
|
||
content:"";
|
||
position:absolute;
|
||
left:2px;
|
||
top:2px;
|
||
width:10px;
|
||
height:10px;
|
||
border-radius:50%;
|
||
background:#888;
|
||
transition:left .12s ease,background .12s ease
|
||
}
|
||
.toggle input:checked{background:#666}
|
||
.toggle input:checked:after{left:12px;background:rgba(255,255,255,.88)}
|
||
.workspace{
|
||
display:grid;
|
||
grid-template-rows:minmax(430px,1fr) 126px;
|
||
gap:8px;
|
||
min-height:calc(100vh - 20px)
|
||
}
|
||
.preview{
|
||
position:relative;
|
||
overflow:hidden;
|
||
isolation:isolate;
|
||
background:#000;
|
||
cursor:crosshair;
|
||
user-select:none
|
||
}
|
||
.preview.grid-on{
|
||
background-image:
|
||
linear-gradient(90deg,#151515 1px,transparent 1px),
|
||
linear-gradient(#151515 1px,transparent 1px);
|
||
background-size:48px 48px
|
||
}
|
||
.map-image{
|
||
position:absolute;
|
||
inset:0;
|
||
width:100%;
|
||
height:100%;
|
||
z-index:-2;
|
||
object-fit:cover;
|
||
opacity:.76;
|
||
filter:saturate(.82) contrast(.92);
|
||
pointer-events:none
|
||
}
|
||
.map-fallback{position:absolute;inset:0;z-index:-3;background:#000;pointer-events:none}
|
||
.preview:before,.preview:after{
|
||
content:"";
|
||
position:absolute;
|
||
z-index:1;
|
||
pointer-events:none;
|
||
background:rgba(255,255,255,.18)
|
||
}
|
||
.preview:before{left:0;right:0;top:50%;height:1px}
|
||
.preview:after{top:0;bottom:0;left:50%;width:1px}
|
||
.map-tag{
|
||
position:absolute;
|
||
left:8px;
|
||
top:8px;
|
||
z-index:4;
|
||
padding:4px 6px;
|
||
background:#000;
|
||
color:rgba(255,255,255,.7);
|
||
font-size:10px;
|
||
letter-spacing:.1em;
|
||
text-transform:uppercase;
|
||
pointer-events:none
|
||
}
|
||
.viewer-tools{position:absolute;right:8px;top:8px;z-index:5;display:flex;gap:5px}
|
||
.tool-button{height:25px;padding:0 8px;font-size:10px}
|
||
.tool-button.active{background:rgba(255,255,255,.84);color:#000}
|
||
.crosshair{position:absolute;left:50%;top:50%;width:0;height:0;z-index:3;pointer-events:none}
|
||
.part,.dot{position:absolute;display:block;background:#fff;opacity:1;box-shadow:none}
|
||
.part{border-radius:0}
|
||
.dot{border-radius:50%;transform:translate(-50%,-50%)}
|
||
.output{position:relative;padding:7px}
|
||
textarea{
|
||
display:block;
|
||
width:100%;
|
||
height:100%;
|
||
min-height:108px;
|
||
resize:none;
|
||
outline:none;
|
||
color:var(--bright);
|
||
background:#000;
|
||
border:0;
|
||
border-radius:0;
|
||
padding:8px 9px 32px;
|
||
font:10px/1.4 "SFMono-Regular",Consolas,"Liberation Mono",monospace
|
||
}
|
||
.actions{position:absolute;right:13px;bottom:13px;display:flex;gap:5px}
|
||
button{
|
||
height:24px;
|
||
padding:0 8px;
|
||
color:var(--soft);
|
||
background:var(--control);
|
||
border:0;
|
||
border-radius:0;
|
||
cursor:pointer;
|
||
font-size:10px
|
||
}
|
||
button:hover{color:#000;background:rgba(255,255,255,.84)}
|
||
.status{position:absolute;left:15px;bottom:17px;color:#777;font-size:10px}
|
||
.map-picker{position:relative}
|
||
.map-button{
|
||
width:100%;
|
||
height:30px;
|
||
padding:2px;
|
||
display:flex;
|
||
align-items:center;
|
||
gap:7px;
|
||
color:var(--bright);
|
||
background:var(--control);
|
||
border:0;
|
||
border-radius:0;
|
||
outline:none;
|
||
cursor:pointer;
|
||
text-align:left;
|
||
font-size:11px
|
||
}
|
||
.map-button:after{content:"+";margin-left:auto;padding:0 6px;color:#777;font-size:15px}
|
||
.map-thumb{width:38px;height:26px;flex:0 0 38px;object-fit:cover;background:#000}
|
||
.map-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.map-list{
|
||
display:none;
|
||
position:absolute;
|
||
left:0;
|
||
right:0;
|
||
top:calc(100% + 4px);
|
||
z-index:20;
|
||
max-height:260px;
|
||
overflow:auto;
|
||
padding:3px;
|
||
background:var(--control);
|
||
border:0;
|
||
border-radius:0
|
||
}
|
||
.map-picker.open .map-list{display:block}
|
||
.map-picker.open .map-button:after{content:"−"}
|
||
.map-option{
|
||
width:100%;
|
||
padding:5px;
|
||
display:flex;
|
||
align-items:center;
|
||
gap:8px;
|
||
border:0;
|
||
border-radius:0;
|
||
background:var(--control);
|
||
color:var(--soft);
|
||
text-align:left;
|
||
cursor:pointer;
|
||
font-size:11px
|
||
}
|
||
.map-option:hover,.map-option.active{color:var(--bright);background:#292929}
|
||
.map-option .map-thumb{width:43px;height:27px;flex-basis:43px}
|
||
.color-inline{display:grid;grid-template-columns:minmax(0,1fr) 29px;gap:5px;align-items:center}
|
||
input[type=color]{
|
||
appearance:none;
|
||
width:29px;
|
||
height:29px;
|
||
padding:3px;
|
||
background:#000;
|
||
border:0;
|
||
border-radius:0;
|
||
cursor:pointer
|
||
}
|
||
input[type=color]::-webkit-color-swatch-wrapper{padding:0}
|
||
input[type=color]::-webkit-color-swatch{border:0;border-radius:0}
|
||
input[type=color]::-moz-color-swatch{border:0;border-radius:0}
|
||
.hex-readout{
|
||
min-height:14px;
|
||
margin-top:4px;
|
||
color:#777;
|
||
font-size:10px;
|
||
letter-spacing:.04em;
|
||
text-transform:uppercase
|
||
}
|
||
.hex-readout[hidden]{display:none}
|
||
@media(max-width:720px){
|
||
body{padding:7px}
|
||
.app{grid-template-columns:1fr;min-height:0;max-width:620px}
|
||
.sidebar{max-height:none;overflow:visible}
|
||
.workspace{min-height:auto;grid-template-rows:410px 124px}
|
||
}
|
||
@media(max-width:400px){
|
||
.toggle-grid{grid-template-columns:1fr}
|
||
.workspace{grid-template-rows:350px 124px}
|
||
.viewer-tools{top:auto;bottom:8px}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app">
|
||
<aside class="panel sidebar">
|
||
<div class="field">
|
||
<div class="map-picker" id="mapPicker">
|
||
<button class="map-button" id="mapButton" type="button" aria-expanded="false">
|
||
<img class="map-thumb" id="selectedMapThumb" alt="">
|
||
<span class="map-name" id="selectedMapName">None / Black</span>
|
||
</button>
|
||
<div class="map-list" id="mapList"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<select id="style" aria-label="Crosshair style">
|
||
<option value="4">Static</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<div class="color-inline">
|
||
<select id="color" aria-label="Crosshair color preset">
|
||
<option value="5">Custom RGB</option>
|
||
<option value="0">Red</option>
|
||
<option value="1">Green</option>
|
||
<option value="2">Yellow</option>
|
||
<option value="3">Blue</option>
|
||
<option value="4">Cyan</option>
|
||
</select>
|
||
<input id="hexColorPicker" type="color" value="#00FFFF" aria-label="Custom crosshair color picker">
|
||
</div>
|
||
<div class="hex-readout" id="hexReadout">#00FFFF</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<div class="label">cl_crosshairsize <span class="value" id="sizeV"></span></div>
|
||
<div class="range-row">
|
||
<input id="size" type="range" min=".5" max="10" step=".1" value="2.5">
|
||
<div class="number-control">
|
||
<button class="step" data-step="size" data-dir="-1" type="button">−</button>
|
||
<input id="sizeN" type="number" min=".5" max="10" step=".1" value="2.5">
|
||
<button class="step" data-step="size" data-dir="1" type="button">+</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<div class="label">cl_crosshairthickness <span class="value" id="thicknessV"></span></div>
|
||
<div class="range-row">
|
||
<input id="thickness" type="range" min=".1" max="6" step=".1" value=".5">
|
||
<div class="number-control">
|
||
<button class="step" data-step="thickness" data-dir="-1" type="button">−</button>
|
||
<input id="thicknessN" type="number" min=".1" max="6" step=".1" value=".5">
|
||
<button class="step" data-step="thickness" data-dir="1" type="button">+</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<div class="label">cl_crosshairgap <span class="value" id="gapV"></span></div>
|
||
<div class="range-row">
|
||
<input id="gap" type="range" min="-10" max="10" step=".1" value="-3">
|
||
<div class="number-control">
|
||
<button class="step" data-step="gap" data-dir="-1" type="button">−</button>
|
||
<input id="gapN" type="number" min="-10" max="10" step=".1" value="-3">
|
||
<button class="step" data-step="gap" data-dir="1" type="button">+</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<div class="label">cl_crosshair_outlinethickness <span class="value" id="outlineTV"></span></div>
|
||
<div class="range-row">
|
||
<input id="outlineT" type="range" min=".1" max="3" step=".1" value="1">
|
||
<div class="number-control">
|
||
<button class="step" data-step="outlineT" data-dir="-1" type="button">−</button>
|
||
<input id="outlineTN" type="number" min=".1" max="3" step=".1" value="1">
|
||
<button class="step" data-step="outlineT" data-dir="1" type="button">+</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<div class="label">cl_crosshaircolor_r <span class="value" id="rV"></span></div>
|
||
<div class="range-row">
|
||
<input id="r" type="range" min="0" max="255" step="1" value="0">
|
||
<div class="number-control">
|
||
<button class="step" data-step="r" data-dir="-1" type="button">−</button>
|
||
<input id="rN" type="number" min="0" max="255" step="1" value="0">
|
||
<button class="step" data-step="r" data-dir="1" type="button">+</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<div class="label">cl_crosshaircolor_g <span class="value" id="gV"></span></div>
|
||
<div class="range-row">
|
||
<input id="g" type="range" min="0" max="255" step="1" value="255">
|
||
<div class="number-control">
|
||
<button class="step" data-step="g" data-dir="-1" type="button">−</button>
|
||
<input id="gN" type="number" min="0" max="255" step="1" value="255">
|
||
<button class="step" data-step="g" data-dir="1" type="button">+</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<div class="label">cl_crosshaircolor_b <span class="value" id="bV"></span></div>
|
||
<div class="range-row">
|
||
<input id="b" type="range" min="0" max="255" step="1" value="255">
|
||
<div class="number-control">
|
||
<button class="step" data-step="b" data-dir="-1" type="button">−</button>
|
||
<input id="bN" type="number" min="0" max="255" step="1" value="255">
|
||
<button class="step" data-step="b" data-dir="1" type="button">+</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<div class="label">cl_crosshairalpha <span class="value" id="alphaV"></span></div>
|
||
<div class="range-row">
|
||
<input id="alpha" type="range" min="0" max="255" step="1" value="255">
|
||
<div class="number-control">
|
||
<button class="step" data-step="alpha" data-dir="-1" type="button">−</button>
|
||
<input id="alphaN" type="number" min="0" max="255" step="1" value="255">
|
||
<button class="step" data-step="alpha" data-dir="1" type="button">+</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<div class="label" style="opacity:0.52;">cl_crosshair_sniper_width <span class="value" style="opacity:0.5;" id="sniperWidthV"></span></div>
|
||
<div class="range-row">
|
||
<input id="sniperWidth" type="range" min="0" max="10" step=".1" value="1" disabled>
|
||
<div class="number-control" style="opacity:0.5;">
|
||
<button class="step" data-step="sniperWidth" data-dir="-1" type="button">−</button>
|
||
<input id="sniperWidthN" type="number" min="0" max="10" step=".1" value="1">
|
||
<button class="step" data-step="sniperWidth" data-dir="1" type="button">+</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<div class="toggle-grid">
|
||
<label class="toggle">center dot <input id="dot" type="checkbox"></label>
|
||
<label class="toggle">outline <input id="outline" type="checkbox"></label>
|
||
<label class="toggle">t style <input id="t" type="checkbox"></label>
|
||
<label class="toggle">weapon gap <input id="deployed" type="checkbox"></label>
|
||
<label class="toggle">use alpha <input id="useAlpha" type="checkbox" checked></label>
|
||
<label class="toggle">follow recoil <input id="recoil" type="checkbox"></label>
|
||
|
||
<label class="toggle" style="background:none;max-width:none;width:100%;">
|
||
<a style="text-decoration:none;font-size:11px;color:#f7f7f7;background:transparent;" href="https://s1ne.com/cs2pattern">spray patterns</a></label>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<main class="workspace">
|
||
<section class="panel preview grid-on" id="preview" aria-label="Crosshair preview">
|
||
<div class="map-fallback" id="mapFallback"></div>
|
||
<img class="map-image" id="mapImage" alt="">
|
||
<span class="map-tag" id="mapTag">No map selected</span>
|
||
|
||
<div class="viewer-tools">
|
||
<button class="tool-button active" id="gridToggle" type="button" aria-pressed="true" style="opacity:0;">Grid: On</button>
|
||
</div>
|
||
|
||
<div class="crosshair" id="crosshair">
|
||
<i class="part" id="top"></i>
|
||
<i class="part" id="bottom"></i>
|
||
<i class="part" id="left"></i>
|
||
<i class="part" id="right"></i>
|
||
<i class="dot" id="dotEl"></i>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel output">
|
||
<textarea id="output" readonly spellcheck="false"></textarea>
|
||
<span class="status" id="status">Autosaved</span>
|
||
<div class="actions">
|
||
<button id="randomize" type="button">Random</button>
|
||
<button id="copy" type="button">Copy</button>
|
||
<button id="reset" type="button">Reset</button>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
</div>
|
||
|
||
<script>
|
||
const $ = id => document.getElementById(id);
|
||
|
||
const STORAGE_KEY = "cs2-classic-static-crosshair-v2";
|
||
|
||
const ranges = [
|
||
"size",
|
||
"thickness",
|
||
"gap",
|
||
"outlineT",
|
||
"r",
|
||
"g",
|
||
"b",
|
||
"alpha",
|
||
"sniperWidth"
|
||
];
|
||
|
||
const checks = [
|
||
"dot",
|
||
"outline",
|
||
"t",
|
||
"deployed",
|
||
"useAlpha",
|
||
"recoil"
|
||
];
|
||
|
||
const colorMap = {
|
||
0: [255, 0, 0],
|
||
1: [0, 255, 0],
|
||
2: [255, 255, 0],
|
||
3: [0, 128, 255],
|
||
4: [0, 255, 255]
|
||
};
|
||
|
||
const maps = [
|
||
{id:"none",name:"None / Black",palette:["#000","#000","#000"],img:""},
|
||
{id:"mirage",name:"Mirage",palette:["#d5a55d","#866344","#32251c"],img:"https://upload.wikimedia.org/wikipedia/commons/thumb/5/5f/Cs2_mirage.jpg/640px-Cs2_mirage.jpg"},
|
||
{id:"dust2",name:"Dust II",palette:["#d4bb86","#907346","#423523"],img:"https://upload.wikimedia.org/wikipedia/commons/thumb/e/e8/Dust_II_A_site.jpg/640px-Dust_II_A_site.jpg"},
|
||
{id:"inferno",name:"Inferno",palette:["#cd9863","#7d5036","#35241c"],img:"https://upload.wikimedia.org/wikipedia/commons/thumb/4/45/Inferno_CS2.jpg/640px-CS2_Inferno.jpg"},
|
||
{id:"nuke",name:"Nuke",palette:["#c5ced0","#6e7c83","#252e33"],img:"https://upload.wikimedia.org/wikipedia/commons/thumb/5/51/CS2_Nuke.jpg/640px-CS2_Nuke.jpg"},
|
||
{id:"ancient",name:"Ancient",palette:["#82975f","#4e633d","#1d2a1e"],img:"https://upload.wikimedia.org/wikipedia/commons/thumb/9/9f/CS2_Ancient.jpg/640px-CS2_Ancient.jpg"},
|
||
{id:"anubis",name:"Anubis",palette:["#cfaa68","#886d45","#352d22"],img:"https://upload.wikimedia.org/wikipedia/commons/thumb/7/71/CS2_Anubis.jpg/640px-CS2_Anubis.jpg"},
|
||
{id:"overpass",name:"Overpass",palette:["#91a078","#5b6652","#29312a"],img:"https://upload.wikimedia.org/wikipedia/commons/thumb/5/5d/CS2_Overpass.jpg/640px-CS2_Overpass.jpg"},
|
||
{id:"vertigo",name:"Vertigo",palette:["#b9c0c3","#73797b","#2c3032"],img:"https://upload.wikimedia.org/wikipedia/commons/thumb/0/0d/CS2_Vertigo.jpg/640px-CS2_Vertigo.jpg"},
|
||
{id:"train",name:"Train",palette:["#849599","#4f5d60","#242e30"],img:"https://upload.wikimedia.org/wikipedia/commons/thumb/8/8a/CS2_Train.jpg/640px-CS2_Train.jpg"}
|
||
];
|
||
|
||
let activeMap = "none";
|
||
let gridEnabled = true;
|
||
|
||
function clamp(value, min, max) {
|
||
const number = Number(value);
|
||
return Number.isFinite(number) ? Math.min(max, Math.max(min, number)) : min;
|
||
}
|
||
|
||
function formatValue(id, value) {
|
||
return ["size", "thickness", "gap", "outlineT", "sniperWidth"].includes(id)
|
||
? Number(value).toFixed(1).replace(/\.0$/, "")
|
||
: String(Math.round(value));
|
||
}
|
||
|
||
function randomBetween(min, max, step = 1) {
|
||
const count = Math.round((max - min) / step);
|
||
return Number((min + Math.floor(Math.random() * (count + 1)) * step).toFixed(4));
|
||
}
|
||
|
||
function setStatus(text) {
|
||
$("status").textContent = text;
|
||
clearTimeout(setStatus.timer);
|
||
|
||
setStatus.timer = setTimeout(() => {
|
||
$("status").textContent = "Saved automatically";
|
||
}, 1400);
|
||
}
|
||
|
||
function rgbToHex(red, green, blue) {
|
||
return "#" + [red, green, blue]
|
||
.map(value => clamp(value, 0, 255).toString(16).padStart(2, "0"))
|
||
.join("")
|
||
.toUpperCase();
|
||
}
|
||
|
||
function hexToRgb(hex) {
|
||
const normalized = hex.trim().replace(/^#/, "");
|
||
|
||
if (!/^[0-9A-Fa-f]{6}$/.test(normalized)) {
|
||
return null;
|
||
}
|
||
|
||
return [
|
||
parseInt(normalized.slice(0, 2), 16),
|
||
parseInt(normalized.slice(2, 4), 16),
|
||
parseInt(normalized.slice(4, 6), 16)
|
||
];
|
||
}
|
||
|
||
function isCustomColor() {
|
||
return $("color").value === "5";
|
||
}
|
||
|
||
function syncColorUi() {
|
||
const hex = rgbToHex($("r").value, $("g").value, $("b").value);
|
||
|
||
$("hexColorPicker").value = hex;
|
||
$("hexReadout").textContent = hex;
|
||
$("hexReadout").hidden = !isCustomColor();
|
||
}
|
||
|
||
function applyHexColor(hex, save = true) {
|
||
const rgb = hexToRgb(hex);
|
||
|
||
if (!rgb) return false;
|
||
|
||
rgb.forEach((value, index) => {
|
||
const id = ["r", "g", "b"][index];
|
||
$(id).value = value;
|
||
$(id + "N").value = value;
|
||
});
|
||
|
||
$("color").value = "5";
|
||
syncColorUi();
|
||
update(save);
|
||
|
||
return true;
|
||
}
|
||
|
||
function mapBackground(map) {
|
||
const [a, b, c] = map.palette;
|
||
|
||
return `
|
||
linear-gradient(90deg,rgba(255,255,255,.04) 1px,transparent 1px) 50% 50%/48px 48px,
|
||
linear-gradient(rgba(255,255,255,.04) 1px,transparent 1px) 50% 50%/48px 48px,
|
||
linear-gradient(135deg,${a},${b} 58%,${c})
|
||
`;
|
||
}
|
||
|
||
function makeThumb(map) {
|
||
const image = document.createElement("img");
|
||
|
||
image.className = "map-thumb";
|
||
image.alt = "";
|
||
image.loading = "lazy";
|
||
image.style.background = map.id === "none"
|
||
? "#000"
|
||
: `linear-gradient(135deg,${map.palette.join(",")})`;
|
||
|
||
if (map.img) image.src = map.img;
|
||
image.onerror = () => image.removeAttribute("src");
|
||
|
||
return image;
|
||
}
|
||
|
||
function cacheMapImages() {
|
||
maps.forEach(map => {
|
||
if (!map.img) return;
|
||
|
||
const image = new Image();
|
||
image.src = map.img;
|
||
});
|
||
}
|
||
|
||
function renderMapList() {
|
||
$("mapList").innerHTML = "";
|
||
|
||
maps.forEach(map => {
|
||
const option = document.createElement("button");
|
||
|
||
option.type = "button";
|
||
option.className = "map-option";
|
||
option.dataset.map = map.id;
|
||
option.append(makeThumb(map));
|
||
|
||
const name = document.createElement("span");
|
||
name.textContent = map.name;
|
||
option.append(name);
|
||
|
||
option.onclick = () => selectMap(map.id);
|
||
$("mapList").append(option);
|
||
});
|
||
}
|
||
|
||
function closeMapPicker() {
|
||
$("mapPicker").classList.remove("open");
|
||
$("mapButton").setAttribute("aria-expanded", "false");
|
||
}
|
||
|
||
function selectMap(id, save = true) {
|
||
const map = maps.find(item => item.id === id) || maps[0];
|
||
|
||
activeMap = map.id;
|
||
|
||
$("mapFallback").style.background = map.id === "none"
|
||
? "#000"
|
||
: mapBackground(map);
|
||
|
||
$("mapTag").textContent = map.name;
|
||
$("selectedMapName").textContent = map.name;
|
||
|
||
const image = $("mapImage");
|
||
|
||
if (map.img) {
|
||
image.src = map.img;
|
||
image.style.opacity = ".76";
|
||
image.onerror = () => image.style.opacity = "0";
|
||
} else {
|
||
image.removeAttribute("src");
|
||
image.style.opacity = "0";
|
||
}
|
||
|
||
const thumb = $("selectedMapThumb");
|
||
|
||
thumb.style.background = map.id === "none"
|
||
? "#000"
|
||
: `linear-gradient(135deg,${map.palette.join(",")})`;
|
||
|
||
if (map.img) {
|
||
thumb.src = map.img;
|
||
thumb.onerror = () => thumb.removeAttribute("src");
|
||
} else {
|
||
thumb.removeAttribute("src");
|
||
}
|
||
|
||
document.querySelectorAll(".map-option").forEach(option => {
|
||
option.classList.toggle("active", option.dataset.map === map.id);
|
||
});
|
||
|
||
closeMapPicker();
|
||
|
||
if (save) saveState();
|
||
}
|
||
|
||
function setGrid(enabled, save = true) {
|
||
gridEnabled = !!enabled;
|
||
|
||
$("preview").classList.toggle("grid-on", gridEnabled);
|
||
$("gridToggle").classList.toggle("active", gridEnabled);
|
||
$("gridToggle").textContent = `Grid: ${gridEnabled ? "On" : "Off"}`;
|
||
$("gridToggle").setAttribute("aria-pressed", String(gridEnabled));
|
||
|
||
if (save) saveState();
|
||
}
|
||
|
||
function saveState() {
|
||
const state = {
|
||
map: activeMap,
|
||
grid: gridEnabled,
|
||
color: $("color").value
|
||
};
|
||
|
||
ranges.forEach(id => state[id] = $(id).value);
|
||
checks.forEach(id => state[id] = $(id).checked);
|
||
|
||
try {
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||
} catch {}
|
||
}
|
||
|
||
function loadState() {
|
||
try {
|
||
const state = JSON.parse(localStorage.getItem(STORAGE_KEY));
|
||
|
||
if (!state) return;
|
||
|
||
if (state.color != null) $("color").value = state.color;
|
||
|
||
ranges.forEach(id => {
|
||
if (state[id] == null) return;
|
||
|
||
const range = $(id);
|
||
const number = $(id + "N");
|
||
const value = clamp(state[id], Number(range.min), Number(range.max));
|
||
|
||
range.value = value;
|
||
number.value = value;
|
||
});
|
||
|
||
checks.forEach(id => {
|
||
if (typeof state[id] === "boolean") {
|
||
$(id).checked = state[id];
|
||
}
|
||
});
|
||
|
||
if (maps.some(map => map.id === state.map)) {
|
||
activeMap = state.map;
|
||
}
|
||
|
||
if (typeof state.grid === "boolean") {
|
||
gridEnabled = state.grid;
|
||
}
|
||
} catch {}
|
||
}
|
||
|
||
function syncRange(id, fromNumber = false) {
|
||
const range = $(id);
|
||
const number = $(id + "N");
|
||
const source = fromNumber ? number : range;
|
||
const value = clamp(source.value, Number(range.min), Number(range.max));
|
||
|
||
range.value = value;
|
||
number.value = value;
|
||
|
||
if (["r", "g", "b"].includes(id)) {
|
||
$("color").value = "5";
|
||
syncColorUi();
|
||
}
|
||
|
||
update();
|
||
}
|
||
|
||
function stepRange(id, direction) {
|
||
const range = $(id);
|
||
const step = Number(range.step) || 1;
|
||
|
||
const value = clamp(
|
||
Number(range.value) + step * direction,
|
||
Number(range.min),
|
||
Number(range.max)
|
||
);
|
||
|
||
range.value = value;
|
||
$(id + "N").value = Number(value.toFixed(4));
|
||
|
||
if (["r", "g", "b"].includes(id)) {
|
||
$("color").value = "5";
|
||
syncColorUi();
|
||
}
|
||
|
||
update();
|
||
}
|
||
|
||
function getCrosshairColor() {
|
||
const preset = colorMap[$("color").value];
|
||
|
||
const [red, green, blue] = preset || [
|
||
Number($("r").value),
|
||
Number($("g").value),
|
||
Number($("b").value)
|
||
];
|
||
|
||
return `rgb(${red},${green},${blue})`;
|
||
}
|
||
|
||
function update(shouldSave = true) {
|
||
ranges.forEach(id => {
|
||
$(id + "V").textContent = formatValue(id, $(id).value);
|
||
});
|
||
|
||
const size = Number($("size").value) * 8;
|
||
const thickness = Math.max(1, Number($("thickness").value) * 3);
|
||
const gap = Number($("gap").value) * 3;
|
||
const rgb = getCrosshairColor();
|
||
|
||
const alpha = $("useAlpha").checked
|
||
? Number($("alpha").value) / 255
|
||
: 1;
|
||
|
||
const outlineEnabled = $("outline").checked;
|
||
const outlineSize = Number($("outlineT").value) * 2;
|
||
|
||
const line = (element, width, height, left, top, hidden = false) => {
|
||
element.style.cssText = `
|
||
display:${hidden ? "none" : "block"};
|
||
width:${width}px;
|
||
height:${height}px;
|
||
left:${left}px;
|
||
top:${top}px;
|
||
background:${rgb};
|
||
opacity:${alpha};
|
||
${outlineEnabled ? `border:${outlineSize}px solid #000;` : ""}
|
||
`;
|
||
};
|
||
|
||
const edge = gap + thickness / 2;
|
||
|
||
line($("top"), thickness, size, -thickness / 2, -edge - size, $("t").checked);
|
||
line($("bottom"), thickness, size, -thickness / 2, edge);
|
||
line($("left"), size, thickness, -edge - size, -thickness / 2);
|
||
line($("right"), size, thickness, edge, -thickness / 2);
|
||
|
||
$("dotEl").style.cssText = `
|
||
display:${$("dot").checked ? "block" : "none"};
|
||
width:${thickness * 2}px;
|
||
height:${thickness * 2}px;
|
||
left:0;
|
||
top:0;
|
||
background:${rgb};
|
||
opacity:${alpha};
|
||
${outlineEnabled ? `border:${outlineSize}px solid #000;` : ""}
|
||
`;
|
||
|
||
$("output").value = [
|
||
"cl_crosshairstyle 4",
|
||
`cl_crosshairsize ${$("size").value}`,
|
||
`cl_crosshairthickness ${$("thickness").value}`,
|
||
`cl_crosshairgap ${$("gap").value}`,
|
||
`cl_crosshairdot ${$("dot").checked ? 1 : 0}`,
|
||
`cl_crosshair_drawoutline ${outlineEnabled ? 1 : 0}`,
|
||
`cl_crosshair_outlinethickness ${$("outlineT").value}`,
|
||
`cl_crosshair_t ${$("t").checked ? 1 : 0}`,
|
||
`cl_crosshairusealpha ${$("useAlpha").checked ? 1 : 0}`,
|
||
`cl_crosshairalpha ${$("alpha").value}`,
|
||
`cl_crosshaircolor ${$("color").value}`,
|
||
`cl_crosshaircolor_r ${$("r").value}`,
|
||
`cl_crosshaircolor_g ${$("g").value}`,
|
||
`cl_crosshaircolor_b ${$("b").value}`,
|
||
`cl_crosshairgap_useweaponvalue ${$("deployed").checked ? 1 : 0}`,
|
||
`cl_crosshair_recoil ${$("recoil").checked ? 1 : 0}`,
|
||
`cl_crosshair_sniper_width ${$("sniperWidth").value}`
|
||
].join("; ");
|
||
|
||
if (shouldSave) saveState();
|
||
}
|
||
|
||
function randomize() {
|
||
const compactPalette = [
|
||
[0,255,255],
|
||
[0,255,0],
|
||
[255,255,0],
|
||
[255,110,0],
|
||
[255,90,180],
|
||
[170,130,255],
|
||
[255,255,255]
|
||
];
|
||
|
||
const rgb = compactPalette[Math.floor(Math.random() * compactPalette.length)];
|
||
|
||
$("size").value = $("sizeN").value = randomBetween(1.5, 5.5, .1);
|
||
$("thickness").value = $("thicknessN").value = randomBetween(.5, 2, .1);
|
||
$("gap").value = $("gapN").value = randomBetween(-5, 2, .1);
|
||
$("outlineT").value = $("outlineTN").value = randomBetween(.5, 1.5, .1);
|
||
$("alpha").value = $("alphaN").value = randomBetween(180, 255, 1);
|
||
$("sniperWidth").value = $("sniperWidthN").value = randomBetween(1, 3, .1);
|
||
|
||
["r", "g", "b"].forEach((id, index) => {
|
||
$(id).value = $(id + "N").value = rgb[index];
|
||
});
|
||
|
||
$("color").value = "5";
|
||
$("dot").checked = Math.random() > .7;
|
||
$("outline").checked = Math.random() > .45;
|
||
$("t").checked = Math.random() > .82;
|
||
$("deployed").checked = false;
|
||
$("useAlpha").checked = true;
|
||
$("recoil").checked = Math.random() > .5;
|
||
|
||
syncColorUi();
|
||
update();
|
||
setStatus("Randomized classic static config");
|
||
}
|
||
|
||
loadState();
|
||
renderMapList();
|
||
selectMap(activeMap, false);
|
||
setGrid(gridEnabled, false);
|
||
cacheMapImages();
|
||
syncColorUi();
|
||
|
||
ranges.forEach(id => {
|
||
$(id).oninput = () => syncRange(id);
|
||
$(id + "N").oninput = () => syncRange(id, true);
|
||
});
|
||
|
||
document.querySelectorAll(".step").forEach(button => {
|
||
button.onclick = () => {
|
||
stepRange(button.dataset.step, Number(button.dataset.dir));
|
||
};
|
||
});
|
||
|
||
["dot", "outline", "t", "deployed", "useAlpha", "recoil"].forEach(id => {
|
||
$(id).oninput = update;
|
||
});
|
||
|
||
$("color").onchange = () => {
|
||
const preset = colorMap[$("color").value];
|
||
|
||
if (preset) {
|
||
preset.forEach((value, index) => {
|
||
const id = ["r", "g", "b"][index];
|
||
$(id).value = value;
|
||
$(id + "N").value = value;
|
||
});
|
||
}
|
||
|
||
syncColorUi();
|
||
update();
|
||
};
|
||
|
||
$("hexColorPicker").oninput = () => {
|
||
applyHexColor($("hexColorPicker").value);
|
||
};
|
||
|
||
$("mapButton").onclick = () => {
|
||
const open = $("mapPicker").classList.toggle("open");
|
||
$("mapButton").setAttribute("aria-expanded", String(open));
|
||
};
|
||
|
||
document.addEventListener("click", event => {
|
||
if (!$("mapPicker").contains(event.target)) {
|
||
closeMapPicker();
|
||
}
|
||
});
|
||
|
||
document.addEventListener("keydown", event => {
|
||
if (event.key === "Escape") {
|
||
closeMapPicker();
|
||
}
|
||
});
|
||
|
||
$("gridToggle").onclick = () => {
|
||
setGrid(!gridEnabled);
|
||
};
|
||
|
||
$("randomize").onclick = randomize;
|
||
|
||
$("copy").onclick = async () => {
|
||
try {
|
||
await navigator.clipboard.writeText($("output").value);
|
||
} catch {
|
||
$("output").focus();
|
||
$("output").select();
|
||
document.execCommand("copy");
|
||
}
|
||
|
||
$("copy").textContent = "Copied";
|
||
setStatus("Commands copied");
|
||
|
||
setTimeout(() => {
|
||
$("copy").textContent = "Copy";
|
||
}, 1100);
|
||
};
|
||
|
||
$("reset").onclick = () => {
|
||
try {
|
||
localStorage.removeItem(STORAGE_KEY);
|
||
} catch {}
|
||
|
||
document.querySelectorAll("input[type=range],input[type=number]").forEach(element => {
|
||
element.value = element.defaultValue;
|
||
});
|
||
|
||
document.querySelectorAll("input[type=checkbox]").forEach(element => {
|
||
element.checked = element.defaultChecked;
|
||
});
|
||
|
||
$("style").value = "4";
|
||
$("color").value = "5";
|
||
activeMap = "none";
|
||
|
||
selectMap("none", false);
|
||
setGrid(true, false);
|
||
syncColorUi();
|
||
update();
|
||
|
||
setStatus("Reset complete");
|
||
};
|
||
|
||
update();
|
||
</script>
|
||
</body>
|
||
</html>
|