|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="ru"> |
| 3 | +<head> |
| 4 | +<meta charset="UTF-8"> |
| 5 | +<title>Graphics</title> |
| 6 | +<style> |
| 7 | + * { margin:0; padding:0; box-sizing:border-box; } |
| 8 | + body { font-family:'Consolas',monospace; display:flex; height:100vh; background:#1e1e1e; color:#d4d4d4; } |
| 9 | + canvas { width:100%; height:100%; cursor:crosshair; } |
| 10 | + #editor { width:35%; display:flex; flex-direction:column; border-right:2px solid #333; } |
| 11 | + #graph { width:65%; position:relative; background:#fff; } |
| 12 | + #editor-header { background:#252526; padding:10px 15px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #333; } |
| 13 | + #editor-header h2 { font-size:14px; color:#4ec9b0; } |
| 14 | + #run-btn { background:#0e639c; color:white; border:none; padding:6px 20px; cursor:pointer; font-family:inherit; font-size:13px; border-radius:3px; } |
| 15 | + #run-btn:hover { background:#1177bb; } |
| 16 | + #code { flex:1; background:#1e1e1e; color:#d4d4d4; border:none; padding:15px; font-family:'Consolas',monospace; font-size:14px; resize:none; outline:none; line-height:1.6; } |
| 17 | + #controls { position:absolute; top:10px; right:10px; background:rgba(255,255,255,0.95); padding:8px; border-radius:5px; } |
| 18 | + #controls button { background:#f0f0f0; border:1px solid #ccc; padding:5px 10px; margin:2px; cursor:pointer; border-radius:3px; font-family:inherit; } |
| 19 | + #info { position:absolute; bottom:10px; left:10px; background:rgba(255,255,255,0.95); padding:8px 12px; border-radius:5px; font-size:12px; color:#333; } |
| 20 | + #msg { position:absolute; bottom:10px; right:10px; padding:8px 12px; border-radius:5px; font-size:12px; display:none; } |
| 21 | + #msg.err { background:rgba(255,0,0,0.9); color:white; } |
| 22 | + #msg.ok { background:rgba(0,200,0,0.9); color:black; } |
| 23 | +</style> |
| 24 | +</head> |
| 25 | +<body> |
| 26 | +<div id="editor"> |
| 27 | + <div id="editor-header"> |
| 28 | + <h2>📝 Editor</h2> |
| 29 | + <button id="run-btn" onclick="runCode()">▶ RUN</button> |
| 30 | + </div> |
| 31 | + <textarea id="code" spellcheck="false"> |
| 32 | +primes = primes(20000); |
| 33 | + |
| 34 | +draw.append(x => pix(x, primes)); |
| 35 | + |
| 36 | +/*for (let i = 0; i < primes.length / 100; i++) { |
| 37 | + const p = primes[i]; |
| 38 | + const f = (x) => x > p ? sin(x * (PI / p)) * p : 0; |
| 39 | + draw.append(x => f(x), { color: randomColor(), width: 1.5 }); |
| 40 | +}*/ |
| 41 | + |
| 42 | +/*for (let i = 0; i < 10.0; i += 0.05) { |
| 43 | + let ecolor = randomColor(); |
| 44 | + draw.append(x => sqrt(pow(x, 3) - 2 * x + 2.0 / i), { color: ecolor }); |
| 45 | + draw.append(x => -sqrt(pow(x, 3) - 2 * x + 2.0 / i), { color: ecolor }); |
| 46 | +}*/ |
| 47 | +</textarea> |
| 48 | +</div> |
| 49 | +<div id="graph"> |
| 50 | + <canvas id="c"></canvas> |
| 51 | + <div id="controls"> |
| 52 | + <button onclick="resetView()">⌂</button> |
| 53 | + </div> |
| 54 | + <div id="info">Graphics: <span id="cnt">0</span> | Scale: <span id="sc">1.0</span></div> |
| 55 | + <div id="msg"></div> |
| 56 | +</div> |
| 57 | + |
| 58 | +<script> |
| 59 | +const canvas = document.getElementById('c'); |
| 60 | +const ctx = canvas.getContext('2d'); |
| 61 | +let scale = 50, cx = 0, cy = 0; |
| 62 | +let graphs = [], dragging = false, lx = 0, ly = 0; |
| 63 | + |
| 64 | +const sin=Math.sin, cos=Math.cos, tan=Math.tan, log=Math.log, log2=Math.log2, log10=Math.log10; |
| 65 | +const sqrt=Math.sqrt, abs=Math.abs, exp=Math.exp, pow=Math.pow, PI=Math.PI, E=Math.E; |
| 66 | +const asin=Math.asin, acos=Math.acos, atan=Math.atan, atan2=Math.atan2; |
| 67 | +const sinh=Math.sinh, cosh=Math.cosh, tanh=Math.tanh; |
| 68 | +const floor=Math.floor, ceil=Math.ceil, round=Math.round, sign=Math.sign; |
| 69 | +const min=Math.min, max=Math.max, random=Math.random; |
| 70 | +function primes(n) { |
| 71 | + const result = []; |
| 72 | + for (let i = 2; i <= n; i++) { |
| 73 | + let isPrime = true; |
| 74 | + for (let j = 2; j * j <= i; j++) { |
| 75 | + if (i % j === 0) { isPrime = false; break; } |
| 76 | + } |
| 77 | + if (isPrime) result.push(i); |
| 78 | + } |
| 79 | + return result; |
| 80 | +} |
| 81 | + |
| 82 | +function pix(x, primes) { |
| 83 | + let left = 0; |
| 84 | + let right = primes.length - 1; |
| 85 | + let result = 0; |
| 86 | + while (left <= right) { |
| 87 | + const mid = Math.floor((left + right) / 2); |
| 88 | + if (primes[mid] <= x) { |
| 89 | + result = mid + 1; |
| 90 | + left = mid + 1; |
| 91 | + } else { |
| 92 | + right = mid - 1; |
| 93 | + } |
| 94 | + } |
| 95 | + return result; |
| 96 | +} |
| 97 | + |
| 98 | + |
| 99 | +function randomColor() { |
| 100 | + const h = Math.floor(Math.random()*360); |
| 101 | + return `hsl(${h},70%,55%)`; |
| 102 | +} |
| 103 | + |
| 104 | +const draw = { |
| 105 | + append: (fn, opts={}) => { |
| 106 | + graphs.push({ |
| 107 | + fn, |
| 108 | + color: opts.color || randomColor(), |
| 109 | + width: opts.width || 2, |
| 110 | + opacity: opts.opacity ?? 0.8 |
| 111 | + }); |
| 112 | + }, |
| 113 | + clear: () => { graphs = []; } |
| 114 | +}; |
| 115 | + |
| 116 | +function resize() { |
| 117 | + const p = document.getElementById('graph'); |
| 118 | + canvas.width = p.clientWidth; |
| 119 | + canvas.height = p.clientHeight; |
| 120 | + render(); |
| 121 | +} |
| 122 | +window.addEventListener('resize', resize); |
| 123 | + |
| 124 | +canvas.addEventListener('mousedown', e => { dragging=true; lx=e.clientX; ly=e.clientY; }); |
| 125 | +canvas.addEventListener('mousemove', e => { |
| 126 | + if (!dragging) return; |
| 127 | + cx -= (e.clientX-lx)/scale; |
| 128 | + cy += (e.clientY-ly)/scale; |
| 129 | + lx=e.clientX; ly=e.clientY; |
| 130 | + render(); |
| 131 | +}); |
| 132 | +canvas.addEventListener('mouseup', () => dragging=false); |
| 133 | +canvas.addEventListener('mouseleave', () => dragging=false); |
| 134 | +canvas.addEventListener('wheel', e => { e.preventDefault(); zoom(e.deltaY>0?0.9:1.1); }); |
| 135 | + |
| 136 | +function zoom(f) { scale *= f; render(); } |
| 137 | +function resetView() { scale=50; cx=0; cy=0; render(); } |
| 138 | + |
| 139 | +function toScreen(x, y) { |
| 140 | + return [(x-cx)*scale + canvas.width/2, canvas.height/2 - (y-cy)*scale]; |
| 141 | +} |
| 142 | + |
| 143 | +function toMath(px, py) { |
| 144 | + return [(px - canvas.width/2)/scale + cx, -(py - canvas.height/2)/scale + cy]; |
| 145 | +} |
| 146 | + |
| 147 | +function render() { |
| 148 | + const W = canvas.width, H = canvas.height; |
| 149 | + ctx.clearRect(0,0,W,H); |
| 150 | + ctx.fillStyle = '#1a1a1a'; |
| 151 | + ctx.fillRect(0,0,W,H); |
| 152 | + const [xMin, yMax] = toMath(0, 0); |
| 153 | + const [xMax, yMin] = toMath(W, H); |
| 154 | + let gridStep = 1; |
| 155 | + const pixelsPerUnit = scale; |
| 156 | + if (pixelsPerUnit < 20) gridStep = 5; |
| 157 | + else if (pixelsPerUnit < 40) gridStep = 2; |
| 158 | + else if (pixelsPerUnit > 200) gridStep = 0.5; |
| 159 | + else if (pixelsPerUnit > 400) gridStep = 0.25; |
| 160 | + ctx.strokeStyle = '#2d2d2d'; |
| 161 | + ctx.lineWidth = 1; |
| 162 | + ctx.beginPath(); |
| 163 | + for (let x = Math.floor(xMin/gridStep)*gridStep; x <= xMax; x += gridStep) { |
| 164 | + const px = (x-cx)*scale + W/2; |
| 165 | + ctx.moveTo(px, 0); |
| 166 | + ctx.lineTo(px, H); |
| 167 | + } |
| 168 | + for (let y = Math.floor(yMin/gridStep)*gridStep; y <= yMax; y += gridStep) { |
| 169 | + const py = H/2 - (y-cy)*scale; |
| 170 | + ctx.moveTo(0, py); |
| 171 | + ctx.lineTo(W, py); |
| 172 | + } |
| 173 | + ctx.stroke(); |
| 174 | + const [axisX_px] = toScreen(0, 0); |
| 175 | + const [, axisY_px] = toScreen(0, 0); |
| 176 | + ctx.strokeStyle = '#545454'; |
| 177 | + ctx.lineWidth = 2; |
| 178 | + ctx.beginPath(); |
| 179 | + if (axisY_px >= 0 && axisY_px <= H) { |
| 180 | + ctx.moveTo(0, axisY_px); |
| 181 | + ctx.lineTo(W, axisY_px); |
| 182 | + ctx.moveTo(W-12, axisY_px-6); |
| 183 | + ctx.lineTo(W, axisY_px); |
| 184 | + ctx.lineTo(W-12, axisY_px+6); |
| 185 | + } |
| 186 | + if (axisX_px >= 0 && axisX_px <= W) { |
| 187 | + ctx.moveTo(axisX_px, H); |
| 188 | + ctx.lineTo(axisX_px, 0); |
| 189 | + ctx.moveTo(axisX_px-6, 12); |
| 190 | + ctx.lineTo(axisX_px, 0); |
| 191 | + ctx.lineTo(axisX_px+6, 12); |
| 192 | + } |
| 193 | + ctx.stroke(); |
| 194 | + ctx.fillStyle = '#555'; |
| 195 | + ctx.font = '11px Consolas'; |
| 196 | + ctx.textAlign = 'center'; |
| 197 | + for (let x = Math.floor(xMin/gridStep)*gridStep; x <= xMax; x += gridStep) { |
| 198 | + if (Math.abs(x) < 0.001) continue; |
| 199 | + const px = (x-cx)*scale + W/2; |
| 200 | + const labelY = Math.max(12, Math.min(H-5, axisY_px + 16)); |
| 201 | + ctx.fillText(parseFloat(x.toFixed(4)).toString(), px, labelY); |
| 202 | + } |
| 203 | + |
| 204 | + ctx.textAlign = 'right'; |
| 205 | + for (let y = Math.floor(yMin/gridStep)*gridStep; y <= yMax; y += gridStep) { |
| 206 | + if (Math.abs(y) < 0.001) continue; |
| 207 | + const py = H/2 - (y-cy)*scale; |
| 208 | + const labelX = Math.max(20, Math.min(W-5, axisX_px - 8)); |
| 209 | + ctx.fillText(parseFloat(y.toFixed(4)).toString(), labelX, py + 4); |
| 210 | + } |
| 211 | + |
| 212 | + if (axisX_px >= 0 && axisX_px <= W && axisY_px >= 0 && axisY_px <= H) { |
| 213 | + ctx.textAlign = 'right'; |
| 214 | + ctx.fillText('0', axisX_px - 8, axisY_px + 16); |
| 215 | + } |
| 216 | + |
| 217 | + graphs.forEach(g => { |
| 218 | + ctx.strokeStyle = g.color; |
| 219 | + ctx.lineWidth = g.width; |
| 220 | + ctx.globalAlpha = g.opacity; |
| 221 | + ctx.beginPath(); |
| 222 | + |
| 223 | + let first = true; |
| 224 | + let prevPy = null; |
| 225 | + const step = 1 / scale; |
| 226 | + for (let px = 0; px <= W; px += 1) { |
| 227 | + const [x] = toMath(px, 0); |
| 228 | + try { |
| 229 | + const y = g.fn(x); |
| 230 | + if (typeof y !== 'number' || !isFinite(y) || Math.abs(y) > 1e8) { |
| 231 | + first = true; |
| 232 | + prevPy = null; |
| 233 | + continue; |
| 234 | + } |
| 235 | + const [, py] = toScreen(x, y); |
| 236 | + if (prevPy !== null && Math.abs(py - prevPy) > H * 0.8) { |
| 237 | + first = true; |
| 238 | + } |
| 239 | + |
| 240 | + if (first) { |
| 241 | + ctx.moveTo(px, py); |
| 242 | + first = false; |
| 243 | + } else { |
| 244 | + ctx.lineTo(px, py); |
| 245 | + } |
| 246 | + prevPy = py; |
| 247 | + } catch(e) { |
| 248 | + first = true; |
| 249 | + prevPy = null; |
| 250 | + } |
| 251 | + } |
| 252 | + ctx.stroke(); |
| 253 | + }); |
| 254 | + |
| 255 | + ctx.globalAlpha = 1; |
| 256 | + document.getElementById('sc').textContent = (scale/50).toFixed(2); |
| 257 | +} |
| 258 | + |
| 259 | +function showMsg(text, type) { |
| 260 | + const m = document.getElementById('msg'); |
| 261 | + m.textContent = text; |
| 262 | + m.className = type; |
| 263 | + m.style.display = 'block'; |
| 264 | + setTimeout(() => m.style.display = 'none', 3000); |
| 265 | +} |
| 266 | + |
| 267 | +function runCode() { |
| 268 | + const code = document.getElementById('code').value; |
| 269 | + graphs = []; |
| 270 | + try { |
| 271 | + const fn = new Function('draw','randomColor','sin','cos','tan','log','log2','log10', |
| 272 | + 'sqrt','abs','exp','pow','PI','E','asin','acos','atan','atan2', |
| 273 | + 'sinh','cosh','tanh','floor','ceil','round','sign','min','max','random','primes', 'pix', |
| 274 | + code); |
| 275 | + fn(draw, randomColor, sin,cos,tan,log,log2,log10, sqrt,abs,exp,pow,PI,E, |
| 276 | + asin,acos,atan,atan2, sinh,cosh,tanh, floor,ceil,round,sign,min,max,random,primes,pix); |
| 277 | + document.getElementById('cnt').textContent = graphs.length; |
| 278 | + render(); |
| 279 | + showMsg(`✓ Build: ${graphs.length}`, 'ok'); |
| 280 | + } catch(e) { |
| 281 | + showMsg(`✗ ${e.message}`, 'err'); |
| 282 | + } |
| 283 | +} |
| 284 | + |
| 285 | +document.getElementById('code').addEventListener('keydown', e => { |
| 286 | + if (e.ctrlKey && e.key === 'Enter') runCode(); |
| 287 | +}); |
| 288 | + |
| 289 | +resize(); |
| 290 | +setTimeout(runCode, 100); |
| 291 | +</script> |
| 292 | +</body> |
| 293 | +</html> |
0 commit comments