HTML5 Canvas Как нарисовать белка с градиентной рамкой? - PullRequest
3 голосов
/ 20 июня 2019

После того, как я много гуглюсь, я не могу найти никаких учебных пособий, в которых объясняется, как рисовать форму сквирта на холсте HTML5, пожалуйста, прости меня, потому что я очень плохо разбираюсь в математике.

Однако я нахожу некоторые похожие / связанные ответы, но я не знаю, как объединить эти знания ...

HTML5 альфа-прозрачность Canvas не работает в Firefox для кривых при большом окне

Непрерывный градиент вдоль пути холста HTML5

https://stackoverflow.com/a/44856925/3896501

Эффект, который я пытаюсь достичь: enter image description here

Спасибо за любую помощь!

ОБНОВЛЕНИЕ 1:

Код, который я создал до сих пор:

<body>
  <div class="con">
    <div class="ava"></div>
    <canvas id="canvas"></canvas>
  </div>
  <script>

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");

    var shadowPadding = 8;
    var strokeWidth = 2;
    canvas.width = canvas.height = (64 + shadowPadding * 2) * window.devicePixelRatio
    canvas.style.width = canvas.style.height = `${canvas.width / window.devicePixelRatio}px`

    function drawMultiRadiantCircle(xc, yc, r, radientColors) {
        var partLength = (2 * Math.PI) / radientColors.length;
        var start = 0;
        var gradient = null;
        var startColor = null,
            endColor = null;

        for (var i = 0; i < radientColors.length; i++) {
            startColor = radientColors[i];
            endColor = radientColors[(i + 1) % radientColors.length];

            // x start / end of the next arc to draw
            var xStart = xc + Math.cos(start) * r;
            var xEnd = xc + Math.cos(start + partLength) * r;
            // y start / end of the next arc to draw
            var yStart = yc + Math.sin(start) * r;
            var yEnd = yc + Math.sin(start + partLength) * r;

            ctx.beginPath();

            gradient = ctx.createLinearGradient(xStart, yStart, xEnd, yEnd);
            gradient.addColorStop(0, startColor);
            gradient.addColorStop(1, endColor);

            ctx.lineWidth = strokeWidth;
            ctx.strokeStyle = gradient;

            // squircle START
            // https://stackoverflow.com/questions/50206406/drawing-a-squircle-shape-on-canvas-android
            // //Formula: (|x|)^3 + (|y|)^3 = radius^3
            // ctx.moveTo(-r, 0);
            // const radiusToPow = r ** 3;
            // const rad = r
            // for (let x = -rad ; x <= rad ; x++)
            //   ctx.lineTo(x + r, Math.cbrt(radiusToPow - Math.abs(x ** 3)) + r);
            // for (let x = rad ; x >= -rad ; x--)
            //   ctx.lineTo(x + r, -Math.cbrt(radiusToPow - Math.abs(x ** 3)) + r);
            // ctx.translate(r, r)
            // ctx.restore()
            // squircle END

            // circle START
            // https://stackoverflow.com/a/22231473/3896501
            ctx.arc(xc, yc, r, start, start + partLength);
            // circle END
            if (i === 1) {
              break
            }
            ctx.stroke();
            ctx.closePath();

            start += partLength;
        }
    }

    var someColors = [];
    someColors.push('#0F0');
    someColors.push('#0FF');
    someColors.push('#F00');
    someColors.push('#FF0');
    someColors.push('#F0F');

    var mid = canvas.width / 2;
    var r = (canvas.width - (shadowPadding * 2)) / 2 + (strokeWidth / 2)
    drawMultiRadiantCircle(mid, mid, r, someColors);

  </script>
  <style>
  .con {
    align-items: center;
    justify-content: center;
    display: flex;
    height: 4rem;
    margin: 6rem;
    width: 4rem;
    position: relative;
  }
  .ava {
    background: #555 50% no-repeat;
    background-size: contain;
    border-radius: 24px;
    height: 100%;
    width: 100%;
  }
  canvas {
    height: 100%;
    width: 100%;
    position: absolute;
  }
  </style>
</body>

рисование части круга с градиентным цветом:

рисование белка:

Я не знаю, как кодировать алгоритм, который рисует часть белка точно так же, как context.arc.

Ответы [ 2 ]

5 голосов
/ 21 июня 2019

Если мы прочитаем статью в Википедии о сквиках , мы увидим, что это просто невзвешенная функция эллипса, использующая степени 2 или выше, что означает, что мы можем довольно легко вычислить значения "y", заданные "x«оценивать и рисовать вещи таким образом, но это даст нам крайне неравные сегменты: небольшие изменения в x приведут к ОГРОМНЫМ изменениям в y в начальной и конечной точках и крошечные изменения в y в средней точке.

Вместо этого давайте смоделируем сквикл как параметрическую функцию, чтобы мы изменили одно управляющее значение и получили разумно равномерно распределенные интервалы для работы.Мы можем найти это объяснение в статье в Википедии о функции суперэллипса :

x = |cos(t)^(2/n)| * sign(cos(t))
y = |sin(t)^(2/n)| * sign(sin(t))

для t от 0 до 2π, а радиусы зафиксированы в 1 (поэтому они исчезают из умножений).

Если мы реализуем это, то мы можем добавить радужную раскраску почти в качестве запоздалой мысли, рисуя каждый сегмент пути отдельно, с помощью раскраски strokeStyle, которая использует цвета HSL, где значения оттенков смещаются на основе нашего t значение:

// alias some math functions so we don't need that "Math." all the time
const abs=Math.abs, sign=Math.sign, sin=Math.sin, cos=Math.cos, pow=Math.pow;

// N=2 YIELDS A CIRCLE, N>2 YIELDS A SQUIRCLE
const n = 4;

function coord(t) {
  let power = 2/n;
  let c = cos(t), x = pow(abs(c), power) * sign(c);
  let s = sin(t), y = pow(abs(s), power) * sign(s);
  return { x, y };
}

function drawSegmentTo(t) {
  let c = coord(t);
  let cx = dim + r * c.x;     // Here, dim is our canvas "radius",
  let cy = dim + r * c.y;     // and r is our circle radius, with
  ctx.lineTo(cx, cy);         // ctx being our canvas context.

  // stroke segment in rainbow colours
  let h = (360 * t)/TAU;
  ctx.strokeStyle = `hsl(${h}, 100%, 50%)`;
  ctx.stroke();  

  // start a new segment at the end point
  ctx.beginPath();
  ctx.moveTo(cx, cy);
}

Затем мы можем использовать это в сочетании с некоторым стандартным кодом API Canvas2D:

const PI = Math.PI,
      TAU = PI * 2,
      edge = 200, // SIZE OF THE CANVAS, IN PIXELS
      dim = edge/2,
      r = dim * 0.9,
      cvs = document.getElementById('draw');

// set up our canvas
cvs.height = cvs.width = edge;
ctx = cvs.getContext('2d');
ctx.lineWidth = 2;
ctx.fillStyle = '#004';
ctx.strokeStyle = 'black';
ctx.fillRect(0, 0, edge, edge);

И после того, как все настройки завершены, код отрисовки действительно прямой.вперед:

// THIS DETERMINES HOW SMOOTH OF A CURVE GETS DRAWN
const segments = 32;

// Peg our starting point, which we know is (r,0) away from the center.
ctx.beginPath();
ctx.moveTo(dim + r, dim)

// Then we generate all the line segments on the path
for (let step=TAU/segments, t=step; t<=TAU; t+=step) drawSegmentTo(t);

// And because IEEE floats are imprecise, the last segment may not
// actually reach our starting point. As such, make sure to draw it!
ctx.lineTo(dim + r, dim);
ctx.stroke();

Выполнение этого приведет к следующей короткой последовательности:

a rainbox squircle of power 4

С jsbin, чтобы вы могли играть с числами: https://jsbin.com/haxeqamilo/edit?js,output

Конечно, вы также можете пойти совершенно другим путем: создать элемент SVG (поскольку SVG является частью HTML5) с элементом <path> и соответствующим образом установить ширину, высоту и поле просмотра, а затем сгенерировать d атрибути градиентный цвет, но это определенно гораздо более привередливый .

4 голосов
/ 24 июня 2019

Имея под рукой математическое выражение, вы можете выполнить полное сканирование ограничивающего прямоугольника и оценить попиксельно, если

  • лежит снаружи
  • это часть границы
  • лежит внутри

Для градиента я бы применил некоторую непрерывную функцию (и) к углу. Как какая-то вещь греха / соз:

let ctx=cnv.getContext("2d");

function gradient(angle){
  return "rgb("+
    (128+127*Math.sin(angle*8))+","+
    (128+127*Math.cos(angle*6))+","+
    (128+127*Math.sin(angle*16))+")";
}

for(let x=0;x<360;x++){
  ctx.fillStyle=gradient(x*Math.PI/180);
  ctx.fillRect(250-180+x,0,1,10);
}

let mx=250,my=90,rx=70,ry=70;

let start=Date.now();

for(let x=-rx;x<=rx;x++)
  for(let y=-ry;y<=ry;y++){
    let r4=Math.pow(x/rx,4)+Math.pow(y/ry,4);
    if(r4<0.8){
      ctx.fillStyle="gray";
      ctx.fillRect(mx+x,my+y,1,1);
    }else if(r4<1){
      ctx.fillStyle=gradient(Math.atan2(x,y));
      ctx.fillRect(mx+x,my+y,1,1);
    }
  }

console.log(Date.now()-start);
<canvas id="cnv" width="500" height="170"></canvas>

Для реального использования этот подход может работать лучше с закадровой композицией в ImageData и, возможно, также с предварительным вычислением градиента:

let ctx=cnv.getContext("2d");

let gradient=new Uint8Array(360*3);

for(let x=0;x<360;x++){
  let r=gradient[x*3]=128+127*Math.sin(x*Math.PI/180*8);
  let g=gradient[x*3+1]=128+127*Math.cos(x*Math.PI/180*6);
  let b=gradient[x*3+2]=128+127*Math.sin(x*Math.PI/180*16);
  ctx.fillStyle="rgb("+r+","+g+","+b+")";
  ctx.fillRect(250-180+x,0,1,10);
}

let mx=250,my=90,rx=70,ry=70;

let start=Date.now();

let imgdata=ctx.createImageData(rx*2+1,ry*2+1);
let data=imgdata.data;

for(let y=-ry,idx=0;y<=ry;y++)
  for(let x=-rx;x<=rx;x++){
    let r4=Math.pow(x/rx,4)+Math.pow(y/ry,4);
    if(r4<0.8){
      data[idx++]=128;
      data[idx++]=128;
      data[idx++]=128;
      data[idx++]=255;
    }else if(r4<1){
      gidx=Math.floor(180+Math.atan2(x,y)*180/Math.PI)%360*3;
      data[idx++]=gradient[gidx++];
      data[idx++]=gradient[gidx++];
      data[idx++]=gradient[gidx++];
      data[idx++]=255;
    }else idx+=4;
  }

ctx.putImageData(imgdata,mx-rx,my-ry);

console.log(Date.now()-start);
<canvas id="cnv" width="500" height="170"></canvas>

На моей машине этот последний вариант медленнее для первого запуска (около 40 мс против 35 мс), но становится значительно быстрее для последующих (14 мс против 31 мс, так что другой не очень ускоряется). Но я не проверял, является ли это результатом ImageData, gradient[] или обоими.


РЕДАКТИРОВАТЬ 06-07-2019 применение предложений, хотя и не вместе ...

Uint32Array делает его короче, проще и быстрее:

let ctx=cnv.getContext("2d");

let gradient=new Uint32Array(360);

for(let x=0;x<360;x++){
  let r=128+127*Math.sin(x*Math.PI/180*8);
  let g=128+127*Math.cos(x*Math.PI/180*6);
  let b=128+127*Math.sin(x*Math.PI/180*16);
  gradient[x]=0xFF000000+(b<<16)+(g<<8)+r;
  ctx.fillStyle="rgb("+r+","+g+","+b+")";
  ctx.fillRect(250-180+x,0,1,10);
}

let mx=250,my=90,rx=70,ry=70;

let start=Date.now();

let imgdata=ctx.createImageData(rx*2+1,ry*2+1);
let data=new Uint32Array(imgdata.data.buffer);

for(let y=-ry,idx=0;y<=ry;y++)
  for(let x=-rx;x<=rx;x++,idx++){
    let r4=Math.pow(x/rx,4)+Math.pow(y/ry,4);
    if(r4<0.8){
      data[idx]=0xFF808080;
    }else if(r4<1){
      gidx=Math.floor(180+Math.atan2(x,y)*180/Math.PI)%360;
      data[idx]=gradient[gidx];
    }
  }

ctx.putImageData(imgdata,mx-rx,my-ry);

console.log(Date.now()-start);
<canvas id="cnv" width="500" height="170"></canvas>

Однако сглаживание не очень тривиально с 32-разрядными числами, поэтому этот вариант возвращается к отдельным компонентам:

let ctx=cnv.getContext("2d");

let gradient=new Uint8Array(360*3);

for(let x=0;x<360;x++){
  let r=gradient[x*3]=128+127*Math.sin(x*Math.PI/180*8);
  let g=gradient[x*3+1]=128+127*Math.cos(x*Math.PI/180*6);
  let b=gradient[x*3+2]=128+127*Math.sin(x*Math.PI/180*16);
  ctx.fillStyle="rgb("+r+","+g+","+b+")";
  ctx.fillRect(250-180+x,0,1,10);
}

let mx=250,my=90,r=70,rr=65;

let start=Date.now();

let imgdata=ctx.createImageData(r*2+1,r*2+1);
let data=imgdata.data;

function mix(a,b,w){
  return b+(a-b)*w;
}

for(let y=-r,idx=0;y<=r;y++)
  for(let x=-r;x<=r;x++){
    let d=Math.pow(Math.pow(x,4)+Math.pow(y,4),0.25);
    if(d<=rr){
      data[idx++]=128;
      data[idx++]=128;
      data[idx++]=128;
      data[idx++]=255;
    }else if(d>=r){
      idx+=4;
    }else{
      let gidx=Math.floor(180+Math.atan2(x,y)*180/Math.PI)%360*3;
      if(d<rr+1){
        let w=d-rr;
        data[idx++]=mix(gradient[gidx++],128,w);
        data[idx++]=mix(gradient[gidx++],128,w);
        data[idx++]=mix(gradient[gidx++],128,w);
        data[idx++]=255;
      }else if(d>r-1){
        let w=r-d;
        data[idx++]=mix(gradient[gidx++],255,w);
        data[idx++]=mix(gradient[gidx++],255,w);
        data[idx++]=mix(gradient[gidx++],255,w);
        data[idx++]=255;
      }else{
        data[idx++]=gradient[gidx++];
        data[idx++]=gradient[gidx++];
        data[idx++]=gradient[gidx++];
        data[idx++]=255;
      }
    }
  }

ctx.putImageData(imgdata,mx-r,my-r);

console.log(Date.now()-start);
<canvas id="cnv" width="500" height="170"></canvas>
...