Нарисуйте контур за пределами поверхности пути

У меня есть следующий код для рисования фигур (в основном используется для прямоугольников), но функции рисования HTML5, похоже, рисуют границы с их толщиной, центрированной по указанным линиям. Я хотел бы иметь границу за пределами поверхности формы, и я в недоумении.

Path.prototype.trace = function(elem, closePath) {
  sd.context.beginPath();
  sd.context.moveTo(this.getStretchedX(0, elem.width), this.getStretchedY(0, elem.height));
  sd.context.lineCap = "square";

  for(var i=1; i<this.points.length; ++i) {
    sd.context.lineTo(this.getStretchedX(i, elem.width), this.getStretchedY(i, elem.height));
  }

  if(closePath) {
    sd.context.lineTo(this.getStretchedX(0, elem.width), this.getStretchedY(0, elem.height));
  }
}

getStrechedX и getStretchY возвращают координаты n-й вершины, как только форма применяется к заданной ширине, высоте и смещению элемента.


Благодаря ответу Кена Фирстенберга у меня это работает для прямоугольника, но это решение, к сожалению, не применимо к другим формам.

http://jsfiddle.net/0zq9mrch/

неудавшиеся треугольники

Здесь я нарисовал две «широкие» границы, одна из которых вычитала половину ширины линии для каждой позиции, а другая добавляла. Это не работает (как и ожидалось), потому что толстые линии будут только сверху и слева в одном случае, под и справа в другом, а не «вне» формы. Вы также можете увидеть белую область вокруг склона.


Я попытался выяснить, как заставить вершины вручную рисовать путь для толстой границы (используя fill() вместо stroke()).

толстый треугольник с показанными вершинами

Но оказывается, я все еще сталкиваюсь с той же проблемой: как программно определить, находится ли ребро внутри или снаружи. Это потребует некоторой тригонометрии и тяжелого алгоритма. Для целей моей текущей работы это слишком большая проблема. Я хотел использовать это, чтобы нарисовать карту здания. Стены комнаты должны быть нарисованы за пределами заданных размеров, но пока я буду придерживаться отдельных наклонных стен.


person Domino    schedule 15.04.2015    source источник
comment
Вы просто рисуете линии, для линии нет понятия внутри или снаружи. Я считаю, что вам просто придется иметь дело с этим самостоятельно и сместить линию на 0,5 ширины линии в любом направлении, которое вы хотите.   -  person James Montagne    schedule 15.04.2015


Ответы (2)


Решение

Вы можете решить эту проблему, нарисовав две линии:

  • Первая линия с толщиной линии, как предполагалось
  • Вторая линия сократилась на 50% ширины внешней линии.

Чтобы сократить, добавьте 50% к x и y, вычтите ширину линии (или 2x 50%) из ширины и высоты.

Пример

snap

var ctx = document.querySelector("canvas").getContext("2d");
var lineWidth = 20;
var lw50 = lineWidth * 0.5;

// outer line
ctx.lineWidth = lineWidth;         // intended line width
ctx.strokeStyle = "#975";          // color for main line
ctx.strokeRect(40, 40, 100, 100);  // full line

// inner line
ctx.lineWidth = 2;                 // inner line width
ctx.strokeStyle = "#000";          // color for inner line

ctx.strokeRect(40 + lw50, 40 + lw50, 100 - lineWidth, 100 - lineWidth);
<canvas></canvas>

Сложные формы

комплекс оснастки

Для более сложных форм вам придется рассчитывать путь вручную. Это немного сложнее и, возможно, слишком широко для SO. Вы должны учитывать такие вещи, как касательные, углы на изгибах, пересечениях и так далее.

Один из способов обмана:

  • нарисуйте основную линию на полную толщину на холсте
  • затем используйте повторное использование пути в качестве обтравочной маски
  • изменить композитный режим на пункт назначения поверх
  • нарисуйте смещение формы в различных направлениях
  • восстановить отсечение
  • измените цвет и снова используйте путь для основной линии.

Значение offset ниже определяет толщину внутренней линии, а значение directions определяет разрешение.

var ctx = document.querySelector("canvas").getContext("2d");
var lineWidth = 20;
var offset = 0.5;                                   // line "thickness"
var directions = 8;                                 // increase to increase details
var angleStep = 2 * Math.PI / 8;

// shape
ctx.lineWidth = lineWidth;                          // intended line width
ctx.strokeStyle = "#000";                           // color for inner line
ctx.moveTo(50, 100);                                // some random shape
ctx.lineTo(100, 20);
ctx.lineTo(200, 100);
ctx.lineTo(300, 100);
ctx.lineTo(200, 200);
ctx.lineTo(50, 100);
ctx.closePath();
ctx.stroke();

ctx.save()

ctx.clip();                                         // set as clipping mask
ctx.globalCompositeOperation = "destination-atop";  // draws "behind" existing drawings

for(var a = 0; a < Math.PI * 2; a += angleStep) {
  ctx.setTransform(1,0,0,1, offset * Math.cos(a), offset * Math.sin(a));
  ctx.drawImage(ctx.canvas, 0, 0);
}

ctx.restore();                              // removes clipping, comp. mode, transforms

// set new color and redraw same path as previous
ctx.strokeStyle = "#975";                           // color for inner line
ctx.stroke();
<canvas height=250></canvas>

person Community    schedule 15.04.2015
comment
На самом деле, я хочу наоборот (форма определяется черной линией, а затем снаружи добавляется коричневая), но я понял идею. Я попробую посмотреть, что это дает мне с другими формами. - person Domino; 15.04.2015
comment
@JacqueGoupil это будет работать только с простыми / прямоугольными формами. Если вам нужны более сложные формы, это может помочь - нарисуйте фигуру на холсте за пределами экрана, а затем используйте подход, аналогичный здесь: stackoverflow.com/ вопросы/25467349/ - person ; 15.04.2015
comment
Да здравствует Кен Фирстенберг! Большое спасибо. - person Domino; 16.04.2015

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

Он использует PathObject для упрощения процесса создания внешнего штриха.

PathObject сохраняет все команды и аргументы, используемые для определения вашего сложного пути.

Этот PathObject также может воспроизводить команды и, таким образом, может переопределять/перерисовывать сохраненный путь.

Класс PathObject можно использовать повторно. Вы можете использовать его для сохранения любого пути (простого или сложного), который вам нужно перерисовать.

Html5 Canvas скоро будет иметь свой собственный объект Path2D, встроенный в контекст, но в моем примере ниже есть кросс-браузерный полифилл, который можно использовать до тех пор, пока объект Path2D не будет реализован.

Иллюстрация облака с серебряной окантовкой, нанесенной внешней обводкой.

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

"Вот как это делается..."

  • Создайте PathObject, в котором можно сохранить все команды и аргументы, используемые для определения вашего сложного пути. Этот PathObject также может воспроизводить команды и тем самым переопределять сохраненный путь. Html5 Canvas скоро будет иметь свой собственный объект Path2D, встроенный в контекст, но мой пример ниже представляет собой кросс-браузерный полифилл, который можно использовать до тех пор, пока не будет реализован объект Path2D.

  • Сохраните сложный путь, используя PathObject.

  • Воспроизведите команды пути на основном холсте и заполните / обведите по желанию.

  • Воспроизведение команд пути на временном холсте в памяти.

  • На временном полотне:

    • Установите context.lineWidth в два раза больше желаемой внешней ширины обводки и выполните обводку.

    • Установите globalCompositeOperation='destination-out' и заполните. Это приведет к тому, что внутренняя часть сложного пути будет очищена и станет прозрачной.

  • Нарисуйте временный холст на основном холсте. Это приводит к тому, что ваш существующий сложный путь на основном холсте получает «внешний штрих» из холста в памяти.

Вот пример кода и демонстрация:

        function log(){console.log.apply(console,arguments);}

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


// A "class" that remembers (and can replay) all the 
// commands & arguments used to define a context path
var PathObject=( function(){

    // Path-related context methods that don't return a value
    var methods = ['arc','beginPath','bezierCurveTo','clip','closePath',
      'lineTo','moveTo','quadraticCurveTo','rect','restore','rotate',
      'save','scale','setTransform','transform','translate','arcTo'];

    var commands=[];
    var args=[];

    function PathObject(){       
        // add methods plus logging
        for (var i=0;i<methods.length;i++){   
            var m = methods[i];
            this[m] = (function(m){
                return function () {
                    if(m=='beginPath'){
                        commands.length=0;
                        args.length=0;
                    }
                    commands.push(m);
                    args.push(arguments);
                    return(this);
            };}(m));
        }
        
        
    };

    // define/redefine the path by issuing all the saved
    //     path commands to the specified context
    PathObject.prototype.definePath=function(context){
        for(var i=0;i<commands.length;i++){
            context[commands[i]].apply(context, args[i]);            
        }
    }   

    //
    PathObject.prototype.show=function(){
        for(var i=0;i<commands.length;i++){
            log(commands[i],args[i]);
        }
    }

    //
    return(PathObject);
})();




var x=75;
var y=100;
var scale=0.50;

// define a cloud path
var path=new PathObject()
.beginPath()
.save()
.translate(x,y)
.scale(scale,scale)
.moveTo(0, 0)
.bezierCurveTo(-40,  20, -40,  70,  60,  70)
.bezierCurveTo(80,  100, 150, 100, 170,  70)
.bezierCurveTo(250,  70, 250,  40, 220,  20)
.bezierCurveTo(260, -40, 200, -50, 170, -30)         
.bezierCurveTo(150, -75,  80, -60,  80, -30)
.bezierCurveTo(30,  -75, -20, -60,   0,   0)
.restore();


// fill the blue sky on the main canvas
ctx.fillStyle='skyblue';
ctx.fillRect(0,0,canvas.width,canvas.height);

// draw the cloud on the main canvas
path.definePath(ctx);
ctx.fillStyle='white';
ctx.fill();
ctx.strokeStyle='black';
ctx.lineWidth=2;
ctx.stroke();

// draw the cloud's silver lining on the temp canvas
path.definePath(ctx1);
ctx1.lineWidth=20;
ctx1.strokeStyle='silver';
ctx1.stroke();
ctx1.globalCompositeOperation='destination-out';
ctx1.fill();

// draw the silver lining onto the main canvas
ctx.drawImage(canvas1,0,0);
body{ background-color: ivory; }
canvas{border:1px solid red;}
<h4>Main canvas with original white cloud + small black stroke<br>The "outside silver lining" is from the temp canvas</h4>
<canvas id="canvas" width=300 height=300></canvas>
<h4>Temporary canvas used to create the "outside stroke"</h4>
<canvas id="canvas1" width=300 height=300></canvas>

person markE    schedule 16.04.2015
comment
Я не собираюсь использовать это решение просто потому, что оно требует от меня добавления еще одного холста буферизации в мое приложение только для одного типа фигуры, но это очень умное решение. Бонусные баллы за создание цепочки функций пути. - person Domino; 16.04.2015