2

Мне часто приходится подбирать вручную координаты контрольных точек кривой Безье, чтобы получить кривые нужной формы. С координатами начальной точки кривой mx, my и конечной точкой x, y dc` всё просто, - какие координаты указаны, там и будут точки.

введите сюда описание изображения

Но координаты контрольных точек X1,Y1 и X2,Y2 нужно подбирать вручную, чтобы получить нужную форму кривой.

Например, для того, чтобы сделать анимацию, использующую несколько взаимосвязанных кривых Безье:

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" 
    xmlns:xlink="http://www.w3.org/1999/xlink"
 width="400" height="400" viewBox="0 0 400 400" &gt;  

<path fill="dodgerblue" stroke="gold" stroke-width="3" d="M70,111 C73,23 241,33 352,111 " > <animate attributeName="d" dur="3s" values="M70,111 C73,23 241,33 352,111; M70,111 C190,58 349,39 352,111; M70,111 C73,23 241,33 352,111" repeatCount="indefinite">

</animate>
</path>
</svg>

Как можно упростить, автоматизировать получение координат контрольных точек кривых Безье?

Alexandr_TT
  • 110,146
  • 23
  • 114
  • 384

2 Answers2

4

Вы можете перемещать мышкой начальную, конечную и контрольные точки кривой Безье, чтобы получить нужную форму кривой. При этом, будут синхронно отображаться координаты этих точек.

Обязательно откройте снипет на полный просмотр

Object.getOwnPropertyNames(Math).map(function(p) {
  window[p] = Math[p];
});

var DIM = 900, EXPF = 1.3, RPF = .028, κ = 1.2, ρ = 4, ζ = document, rp = round(RPF*DIM), np = 4, demo, ε, svg = ζ.querySelector('svg'), rect, curve, msl = null, mdrg = null;

var Meas = function(id, c0) { var bg, lbl, cc, c0 = c0 || 0, id = id;

this.select = function() { if(lbl) lbl.classList.toggle('sel'); if(bg) bg.classList.toggle('sel'); if(cc) cc.classList.toggle('sel'); };

this.init = (function() { var bb, r, pad, ptt, attr, sel;

lbl = ζ.getElementById('lbl-' + id);

if(lbl) { lbl.textContent = DIM; bg = ζ.getElementById('bg-' + id); bb = lbl.getBBox(); c0 -= 2(κ - 1)bb.height; attr = (id === 'w') ? 'y' : 'x'; lbl.setAttribute(attr, c0);

if(bg) { bb = lbl.getBBox(); r = .4RPFDIM; pad = .5(κ - 1)bb.height; ptt = 2*pad;

bg._attr({
 'x': round(bb.x - pad), 
 'y': round(bb.y - pad), 
 'width': round(bb.width + ptt), 
 'height': round(bb.height + ptt), 
 'rx': r, 'ry': r
});

}

sel = '.token--val .hl--' + id; cc = ζ.querySelector(sel); } })(); };

var Point = function(el, x, y, r) { var el = el, lbl, bg, on, cc, q = 0, r = r || rp, cx, cy, selected = false;

this.moveTo = function(x, y) { var cls, _new, β, bb, ptt, pad;

cx = x || 0; cy = y || 0;

if(el) el._attr({'cx': cx, 'cy': cy});

if(lbl) { if(el.id.indexOf('p') !== -1) { β = atan2(y, x); if(β < 0) β += 2PI; q = ~~(2β/PI)

cls = lbl.className.baseVal;
_new = 'rq' + q;

if(cls.indexOf('rq') &lt; 0) {
 cls += ' ' + _new;
}
else {
 cls = cls.replace(/rq./, _new);
}

lbl.className.baseVal = 
 lbl.className.animVal = cls;

}

this.posLbl((q + .5).5PI); this.updateLbl([x, y]);

if(bg) { bb = lbl.getBBox(); pad = .5(κ - 1)bb.height; ptt = 2*pad; bg._attr({ 'x': round(bb.x - pad), 'y': round(bb.y - pad), 'width': round(bb.width + ptt), 'height': round(bb.height + ptt) }); } }

if(on) { on._attr({'x': cx, 'y': cy}) }

if(cc) { cc.textContent = [cx, cy]; } };

this.updateLbl = function(txt) { if(lbl) lbl.textContent = txt; };

this.posLbl = function(θ) { if(lbl) { lbl._attr({ 'x': 1 + ~~(cx + 2rcos(θ)), 'y': 1 + ~~(cy + 2rsin(θ)) }); } };

this.attachCode = function(s) { var sel, s = s || ',';

if(el) { sel = '.token--val .hl--' + el.id; cc = ζ.querySelector(sel); cc.textContent = [cx, cy].join(s); } };

this.coord = function(f) { if(f) return [cx, cy]; return {'x': cx, 'y': cy}; };

this.select = function() { selected = !selected;

if(el) { el.classList.toggle('sel');

if(lbl) { lbl.classList.toggle('sel'); } if(on) { on.classList.toggle('sel'); } if(bg) { bg.classList.toggle('sel'); } if(cc) { cc.classList.toggle('sel'); } } };

this.selected = function() { return selected; };

this.init = (function() { var lid, onid, bgid;

if(el) { el._attr({'r': r}); lid = 'lbl-' + el.id; lbl = ζ.getElementById(lid); bgid = 'bg-' + el.id; bg = ζ.getElementById(bgid);

if(bg) { bg._attr({ 'rx': .4r, 'ry': .4r }); }

onid = 'on-' + el.id; on = ζ.getElementById(onid); } })();

this.moveTo(x, y); };

var Guide = function(el, points) { var el = el, points = points, nl = points.length;

this.movePointTo = function(i, x, y) { var c;

if(el) { c = el._attr('points').split(' '); c[i] = [x, y].join(); el._attr({'points': c.join(' ')}); }

points[i].moveTo(x, y); };

this.points = function() { return points; };

this.init = (function() { var coords = [];

for(var i = 0; i < nl; i++) { coords.push(points[i].coord(1)); }

if(el) { el._attr({ 'points': coords.join(' ') }); } })(); };

var CCurve = function(el, guide) { var el = el, guide = guide, points = guide.points(), nl = points.length, prf = ['M', 'C', ''];

this.movePointTo = function(i, x, y) { var d, p;

if(el) { d = el._attr('d').split(' '); p = d[i].charAt(0); p = (p === 'M' || p === 'C') ? p : ''; d[i] = p + [x, y]; el._attr({'d': d.join(' ')}); }

guide.movePointTo(i, x, y); };

this.select = function(i) { points[i].select(); };

this.points = function() { return points; };

this.init = (function() { var d = [], c;

if(el) { for(var i = 0; i < nl; i++) { c = points[i].coord(1); d.push(prf[min(2, i)] + c); }

el._attr({'d': d.join(' ')}); } })(); };

var Demo = function() { var tl, orig, a0, a1, meas = {};

this.tl = function() { return tl; };

this.size = function() { var r = ρε, w = 5r, h=4*r;

a0._attr({ 'markerWidth': w, 'markerHeight': h }); a1._attr({ 'markerWidth': w, 'markerHeight': h }); };

this.select = function(id) { meas[id].select(); };

this.meas = function(id) { return meas[id]; };

this.arrow = (function(){ a0 = ζ.getElementById('arrow0'), a1 = ζ.getElementById('arrow1'); })();

this.viewBox = (function() { var sz = EXPFDIM, o = -.5sz, vb_tl = ζ.getElementById('vb-tl'), vb_w = ζ.getElementById('vb-w'), vb_h = ζ.getElementById('vb-h'), tlel = ζ.getElementById('tl');

svg._attr({ 'viewBox': [o, o, sz, sz].join(' ') });

o = -.5*DIM; vb_tl.textContent = [o, o].join(' '); vb_w.textContent = vb_h.textContent = DIM;

tl = new Point(tlel, o, o, round(.35*rp)); tl.attachCode(' '); })();

this.delim = (function() { var rd = ζ.getElementById('delim'), invp = round(100/EXPF), o = round(-.5*invp);

rd._attr({ 'x': o + '%', 'y': o + '%', 'width': invp + '%', 'height': invp + '%' }); })();

this.axes = (function() { var cf = (EXPF - 1)/1.5 + 1, c1 = round(50/cf) + '%', c2 = '-' + c1, ax = ζ.getElementById('axis-x'), ay = ζ.getElementById('axis-y');

ax._attr({'x1': c1, 'x2': c2}); ay._attr({'y1': c1, 'y2': c2}); })();

this.origin = (function() { var el = ζ.getElementById('o'); orig = new Point(el, 0, 0, round(.25*rp)); })();

this.minit = (function() { var f = (EXPF - 1)/6, cf = -(.5 + f), o = .5DIM, c0 = cfDIM, bb, w = ζ.getElementById('w'), h = ζ.getElementById('h');

w._attr({ 'x1': -o, 'y1': c0, 'x2': o, 'y2': c0 }); h._attr({ 'y1': -o, 'x1': c0, 'y2': o, 'x2': c0 });

meas.w = new Meas('w', c0); meas.h = new Meas('h', c0); })(); };

Node.prototype._attr = function(a) { if(typeof a === 'string') return this.getAttribute(a); if(typeof a === 'object') { for(p in a) { this.setAttribute(p, a[p]); } } };

var rand = function(max, min, int) { var max = ((max - 1) || 0) + 1, min = min || 0, gen = min + (max - min)*random();

return int ? (~~gen) : gen; };

var size = function() { rect = svg.getBoundingClientRect(); ε = (EXPF*DIM)/rect.width; demo.size(); };

var toggleSel = function(e) { var t = e.target, cl = t.className, i;

if(cl) { if(typeof cl === 'object') { cl = cl.baseVal; }

if(cl.indexOf('hl--p') !== -1) { i = ~~cl.match(/[0-9]/)[0]; msl = curve.points()[i]; curve.select(i); return; } }

if(t.id.indexOf('tl') !== -1) { msl = demo.tl(); demo.tl().select(); return; }

i = t.id.match(/-[w|h]$/);

if(i) { i = i[0].charAt(1); demo.select(i); msl = demo.meas(i); } };

(function init() { var pts = [], g, el, r = .4DIM, e = .3r, β, x, y

demo = new Demo; size();

for(var i = 0; i < np; i++) { el = ζ.getElementById('p' + i); β = (i + 1.5).5PI; x = round(rcos(β) + rand(e, -e)); y = round(rsin(β) + rand(e, -e)); pts.push(new Point(el, x, y)); pts[i].attachCode(); };

el = ζ.getElementById('guide'); g = new Guide(el, pts);

el = ζ.getElementById('curve'); curve = new CCurve(el, g); })();

addEventListener('resize', size, false);

addEventListener('mouseover', toggleSel, false);

addEventListener('mousedown', function(e) { var t = e.target;

if(t.id.match(/^p[0-3]/)) { mdrg = ~~t.id.charAt(1); } }, false);

addEventListener('mousemove', function(e) { var x, y;

if(mdrg != null) { x = (e.clientX - rect.left)/rect.width ; x = round(EXPFDIM(x- .5)); y = (e.clientY - rect.top)/rect.height; y = round(EXPFDIM(y - .5)); curve.movePointTo(mdrg, x, y); } }, false);

addEventListener('mouseup', function(e) { if(mdrg != null) { mdrg = null; } }, false);

addEventListener('mouseout', toggleSel, false);

body {
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow-y: hidden;
  margin: 0;
  height: 100vh;
  min-width: 320px;
  background: #000;
  font: 0.875em/ 1.75 courier, monospace;
}
body:after {
  position: absolute;
  bottom: 0;
  width: 100%;
  font: 1.5em/3 comic sans ms, sans-serif;
  text-align: center;
  content: "interactive demo: hover numbers, drag points";
}

/* ======= COMMON ======= */
[id*='vb'] {
  color: #bd8a00;
}
[id*='vb'][class*='token'][class*='sel'] {
  background: #bd8a00;
  color: #fff;
}

[class*='p0'], [id*='p0'] {
  color: #e38f81;
}
[class*='p0'][class*='token'][class*='sel'], [id*='p0'][class*='token'][class*='sel'] {
  background: #d14730;
  color: #fff;
}
svg [class*='p0'][class*='sel'], svg [id*='p0'][class*='sel'] {
  color: #d14730;
}

[class*='p1'], [id*='p1'] {
  color: #c58fd4;
}
[class*='p1'][class*='token'][class*='sel'], [id*='p1'][class*='token'][class*='sel'] {
  background: #a048b9;
  color: #fff;
}
svg [class*='p1'][class*='sel'], svg [id*='p1'][class*='sel'] {
  color: #a048b9;
}

[class*='p2'], [id*='p2'] {
  color: #75c4ea;
}
[class*='p2'][class*='token'][class*='sel'], [id*='p2'][class*='token'][class*='sel'] {
  background: #1c9edc;
  color: #fff;
}
svg [class*='p2'][class*='sel'], svg [id*='p2'][class*='sel'] {
  color: #1c9edc;
}

[class*='p3'], [id*='p3'] {
  color: #8cd392;
}
[class*='p3'][class*='token'][class*='sel'], [id*='p3'][class*='token'][class*='sel'] {
  background: #43b74d;
  color: #fff;
}
svg [class*='p3'][class*='sel'], svg [id*='p3'][class*='sel'] {
  color: #43b74d;
}

/* ======= CODE BOX ======= */
/* ------- General ------- */
pre {
  align-self: center;
  box-sizing: border-box;
  margin: 0;
  padding: 0.5em 1em;
  max-width: 100vw;
  width: calc(100vh - 7.125em);
  background: #000;
  color: #fff;
  font: inherit;
}
@media (min-height: 45em) {
  pre {
    width: 37.875em;
  }
}

.token--val span {
  display: inline-block;
  margin: 0 1px;
  padding: 0 2px;
  border-radius: 3px;
  font-weight: 900;
  transition: 0.3s;
  cursor: pointer;
}

/* ======= GRAPHICS ======= */
/* ------- Layout ------- */
section {
  flex: 1;
  background: #fff;
}

svg {
  display: block;
  overflow: visible;
  margin: 0 auto;
  max-width: 100vw;
  max-height: 100vw;
  width: calc(100vh - 7.125em);
  height: calc(100vh - 7.125em);
  min-width: 320px;
  min-height: 320px;
  box-shadow: 0 0 2px;
}
@media (min-height: 45em) {
  svg {
    width: 37.875em;
    height: 37.875em;
  }
}

/* ------- Generic ------- */
* {
  vector-effect: non-scaling-stroke;
}

rect, polyline, path, line {
  fill: none;
  stroke: currentColor;
}

circle {
  fill: currentColor;
  cursor: pointer;
}

text {
  font: 700 2.5em courier, monospace;
  -webkit-user-select: none;
  /* Chrome/Safari/Opera */
  -moz-user-select: none;
  /* Firefox */
  -ms-user-select: none;
  /* IE/Edge */
  -webkit-touch-callout: none;
  /* iOS Safari */
  user-select: none;
  cursor: pointer;
}

tspan {
  font-size: .75em;
}

.rq0 {
  dominant-baseline: hanging;
}

.rq1 {
  text-anchor: end;
  dominant-baseline: hanging;
}

.rq2 {
  text-anchor: end;
}

.rev {
  opacity: .001;
  transition: opacity 0.3s;
}

.sel {
  opacity: .999;
}

svg [id*='p']:not(circle):not(g) {
  pointer-events: none;
}

/* ------- Secondary stuff ------- */
[id='delim'] {
  color: #ddd;
  stroke-dasharray: 10px;
  stroke-width: 2px;
}

[id*='axis-'] {
  marker-start: url(#arrow0);
}

[id='o']:hover + .rev {
  opacity: .999;
}

[id='topleft'] text {
  fill: #fff;
}
[id='topleft'] rect, [id='topleft'] .sel {
  color: #bd8a00;
}

[id='meas'] line {
  marker-start: url(#arrow0);
  marker-end: url(#arrow1);
}
[id='meas'] text {
  fill: currentColor;
  color: #bd8a00;
  pointer-events: none;
}
[id='meas'] text.sel {
  color: #fff;
}
[id='meas'] rect {
  fill: currentColor;
  color: rgba(189, 138, 0, 0);
  cursor: pointer;
}
[id='meas'] rect.sel {
  color: #bd8a00;
}

[id='lbl-w'] {
  text-anchor: middle;
}

[id='lbl-h'] {
  text-anchor: end;
  dominant-baseline: middle;
}

/* ------- Main stuff ------- */
[id='guide'] {
  color: #bbb;
  stroke-width: 2px;
}

[id='curve'] {
  color: #c4605f;
  stroke-width: 3px;
}

circle[id^='p'] {
  fill: #ddd;
  stroke: currentColor;
  stroke-width: 3px;
  transition: fill 0.3s;
}
circle[id^='p'][class*='sel'] {
  fill: currentColor;
}

[id^='on'] {
  text-anchor: middle;
  dominant-baseline: middle;
  font: italic 700 2em trebuchet ms,  arial, sans-serif;
}
[id^='on'][class*='sel'] {
  fill: #fff;
}

[id^='bg'] {
  fill: currentColor;
}
[id='points'] [id^='bg'] + text {
  fill: #fff;
}
<pre>&lt;<span class='token--tag'>svg</span> <span class='token--attr'>viewBox</span>=<span class='token--val'>'<span id='vb-tl' class='token--coord hl--tl'></span> <span id='vb-w' class='token--dim hl--w'></span> <span id='vb-h' class='token--dim hl--h'></span>'</span>>
  &lt;<span class='token--tag'>path</span> <span class='token--attr'>d</span>=<span class='token--val'>'M<span class='token--coord hl--p0'></span><br>           C<span class='token--coord hl--p1'></span> <span class='token--coord hl--p2'></span> <span class='token--coord hl--p3'></span>'</span>/>
&lt;/<span class='token--tag'>svg</span>></pre>

<section>
 <svg>
  <defs>
   <marker id='arrow0' orient='auto' 
       viewBox='-20 -7 25 14' 
       refX='-15'>
    <polygon points='-5,0 0,-5 -15,0 0,5'/>
   </marker>
   <marker id='arrow1' orient='auto' 
       viewBox='-5 -7 25 14' 
       refX='10'>
    <polygon points='5,0 0,-5 15,0 0,5'/>
   </marker>
  </defs>

  <rect id='delim'/>

  <g id='axes'>
   <line id='axis-x'/>
   <line id='axis-y'/>
  </g>

  <g id='orig'>
   <circle id='o'/>
   <text id='lbl-o' class='rq0 rev'>0,0</text>
  </g>

  <g id='topleft'>
   <circle id='tl'/>
   <rect id='bg-tl' class='rev'/>
   <text id='lbl-tl' class='hl--tl rq0 rev'></text>
  </g>

  <g id='meas'>
   <line id='w'/>
   <rect id='bg-w'/>
   <text id='lbl-w' class='hl--w'></text>

   <line id='h'/>
   <rect id='bg-h'/>
   <text id='lbl-h' class='hl--h'></text>
  </g>

  <polyline id='guide'/>

  <path id='curve'/>

  <g id='points'>
   <circle id='p0' class='hl--p0'/>
   <text id='on-p0'>S</text>
   <circle id='p1' class='hl--p1'/>
   <text id='on-p1'>C<tspan>1</tspan></text>
   <circle id='p2' class='hl--p2'/>
   <text id='on-p2'>C<tspan>2</tspan></text>
   <circle id='p3' class='hl--p3'/>
   <text id='on-p3'>E</text>

   <rect id='bg-p0' class='rev'/>
   <text id='lbl-p0' class='rev'></text>
   <rect id='bg-p1' class='rev'/>
   <text id='lbl-p1' class='rev'></text>
   <rect id='bg-p2' class='rev'/>
   <text id='lbl-p2' class='rev'></text>
   <rect id='bg-p3' class='rev'/>
   <text id='lbl-p3' class='rev'></text>
  </g>
 </svg>
</section>

Свободный перевод вопроса expand a pattern in svg? от участника @sonia maklouf.

Alexandr_TT
  • 110,146
  • 23
  • 114
  • 384
  • 1
    То же самое можно сделать с помощью канвас... Слишком много кода для такой ерунды. – Leonid Mar 31 '20 at 17:04
3

let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
let w = canvas.width = 500;
let h = canvas.height = 500;

let points = [ [-450, 450], [-300, -200], [300, -200], [450, 450] ];

let circles = []; let moving = false; let cur = 0; let curDel = [];

canvas.addEventListener('mousedown', e => { cur = circles.findIndex(c => ctx.isPointInPath(c, e.offsetX - 250 , e.offsetY - 250)); if(cur >= 0){ curDel = [points[cur][0]/2 - e.offsetX + 250, points[cur][1]/2 - e.offsetY + 250]; moving = true; } })

canvas.addEventListener('mousemove', e => { if(moving){ points[cur] = [(e.offsetX + curDel[0] - 250)2, (e.offsetY + curDel[1] - 250)2]; render(); } })

document.addEventListener('mouseup', () => { moving = false; })

function render(){
ctx.fillStyle = '#e0e0e0'; ctx.fillRect(0, 0, w, h); ctx.strokeStyle = 'rgba(255,0,0,0.6)'; ctx.stroke(new Path2D('M 250 0 L 250 500')); ctx.stroke(new Path2D('M 0 250 L 500 250'));

ctx.save(); ctx.translate(250, 250); ctx.fillStyle = 'blue';

points.forEach((p,i) => { let circle = new Path2D(); circle.arc(p[0]/2, p[1]/2, 10, 0, 2*Math.PI); ctx.fill(circle); circles[i] = circle; })

ctx.stroke(new Path2D(M ${points[0][0]/2} ${points[0][1]/2} L ${points[1][0]/2} ${points[1][1]/2} L ${points[2][0]/2} ${points[2][1]/2} L ${points[3][0]/2} ${points[3][1]/2}));

let curve = new Path2D(M ${points[0][0]/2} ${points[0][1]/2} C ${points[1][0]/2} ${points[1][1]/2} ${points[2][0]/2} ${points[2][1]/2} ${points[3][0]/2} ${points[3][1]/2});

ctx.strokeStyle = 'rgba(0,0,255,0.6)'; ctx.lineWidth = 3; ctx.stroke(curve);

ctx.restore();

let div = document.getElementById('pathformula'); let text = M ${points[0][0]} ${points[0][1]} C ${points[1][0]} ${points[1][1]} ${points[2][0]} ${points[2][1]} ${points[3][0]} ${points[3][1]}; div.textContent = text;

}

render();

<div id="pathformula"></div>
<canvas id="canvas"></canvas>
Leonid
  • 5,797
  • Кстати, зачем вообще нужно было оси координат смещать в центр, с этим возни только, было бы проще если б как на странице: левый верхний угол - 0,0 – Leonid Mar 31 '20 at 19:52
  • неплохо, действенный способ определения что под курсором =) однако я больше люблю способ с пикинг-буфером, еще не очень аккуратно сделано перемещение точек, все-таки надо учитывать начальное взаиморасположение мышки и круга – Stranger in the Q Mar 31 '20 at 20:14
  • а вообще бы я делал это на svg =) – Stranger in the Q Mar 31 '20 at 20:17
  • @StrangerintheQ на счёт мышки и круга? Дельту перемещения брать, а не за центр принимать? Пикинг буфер? Почему на SVG? – Leonid Mar 31 '20 at 20:25
  • 1
    Всё, точки теперь аккуратно беру, только из-за этих преобразований (смещение координат и масштабирование) как-то все малочитаемо(( – Leonid Mar 31 '20 at 20:44
  • про пикинг буфер https://ru.stackoverflow.com/a/988105/188366 – Stranger in the Q Mar 31 '20 at 20:49
  • Да, способ хитроумный, я изучил. Интересно, как бы он реализовал такое в 3D)) – Leonid Mar 31 '20 at 20:59
  • в 3д так же отлично работает, так сделано наведение в одном из глобусов на java(worldwind), оттуда и узнал о нем, сидя в отдадчике.., его прелесть в том что формы могут быть любые, в том числе и измененные шейдерами, т.е. отличные от геометрии по версии цпу – Stranger in the Q Mar 31 '20 at 21:00
  • В 3Д перекрытие может быть, наложения. – Leonid Mar 31 '20 at 21:01
  • буфер глубины так же все отрабатывет, а еще он относительно дешевый, по сравнению с рэйкастингом – Stranger in the Q Mar 31 '20 at 21:02
  • Спасибо, изучу вопрос, интересно. Я вот все мучаюсь над качественным рендерингом в WebGL, не для анимации, а для фотореалистичной картинки, как обрабатывается в 3D max, возможно это? Расчет отраженного света... – Leonid Mar 31 '20 at 21:07
  • про raymarching слыхали? http://iquilezles.org/www/articles/normalsSDF/normalsSDF.htm – Stranger in the Q Mar 31 '20 at 21:08
  • Нет, я года два как к клаве не прикасался)) Всё забыл, а этого и не знал. – Leonid Mar 31 '20 at 21:10
  • много математики, результат соответствующий, правда видео карта спасибо не скажет ибо огромный фрагментный шейдер, полноэкранный проход – Stranger in the Q Mar 31 '20 at 21:13
  • Я посмотрел, вроде только из нативных приложений доступ есть... Я для браузера смотрю или Electron. Или я ничего не понял? Всегда считал, что браузер полноценный доступ к видеокарте не имеет. – Leonid Mar 31 '20 at 21:17
  • ну как Вам сказать, вот он https://codepen.io/strangerintheq/pen/yWEEgG и вот https://codepen.io/strangerintheq/pen/KKKEwMd – Stranger in the Q Mar 31 '20 at 21:20
  • Я three.js пользовался, все это понятно, но мне нужно, чтобы грубо говоря кадр Аватара отрендерился, а в основном все построено на взаимодействии, на анимации. 60 кадров в секунду, а надо 1 кадр в час)))) – Leonid Mar 31 '20 at 21:26