У меня есть элемент холста. Он рисует несколько линий на основе точки схода искусства.
Я пытаюсь нарисовать дом (пока это просто коробка) из единственной точки схода. Размер коробки определяется переменной delta. Если я изменю значение вручную, произойдет следующее:
Я хотел иметь слайдер, изменяющий переменную delta. Но я получаю действительно странные эффекты. А именно линии выводятся за пределы кадра вправо. Я выкинул повсюду операторы console.info, но все еще не могу найти проблему (как можно даже отладить проблемы с холстом?)
var canvas = document.querySelector("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.width = 1600;
canvas.height = 800;
var ct = canvas.getContext("2d");
// TODO
// 1. Make a center point
// 2. Draw lines jutting from center
// 3. Draw a line parallel to canvas bottom
// 4. Draw an adjoining item upward
// x, y
// right, down
// Nomenclature
// x0a
// coordinate type, vanishingPt#, endPtName
// Vanishing point 0
var x0 = 400;
var y0 = 400;
// Vanishing point end 0a
var x0a = 0;
var y0a = 2 * y0;
// Vanishing point end 0b
var x0b = 2 * x0;
var y0b = 2 * y0;
// Define delta
var delta = 700;
function init() {
console.info(delta, "delta");
console.info(x0b, "x0b");
console.info(y0b, "y0b");
console.info(x0, "x0");
console.info(y0, "y0");
// First Line
ct.beginPath();
ct.moveTo(x0, y0);
ct.lineTo(x0a, y0a);
ct.strokeStyle = 'red';
ct.stroke();
// Second Line
ct.beginPath();
ct.moveTo(x0, y0);
ct.lineTo(x0b, x0b);
ct.strokeStyle = 'green';
ct.stroke();
// House based on second Line
ct.beginPath();
ct.moveTo(x0b, y0b); // starting point
ct.lineTo(x0b + delta, y0b); // right x+100
ct.lineTo(x0b + delta, y0b - delta); // up y-100
ct.lineTo(x0b, y0b - delta); // left x-100
ct.lineTo(x0b, y0b); // down y+100
ct.lineTo(x0b, y0b - delta); // back up y-100
//calculate
ct.lineTo(x0, y0);
ct.lineTo(x0b + delta, y0b - delta);
ct.strokeStyle = 'blue';
ct.stroke();
}
init();
var slider = document.getElementById("myRange");
slider.oninput = function () {
delta = this.value;
requestAnimationFrame(init()); // redraw everything
}body {
background-color: lightgray;
display: flex;
justify-content: center;
align-items: center;
}
.slideContainer {
position: fixed;
right: 30px;
top: 30px;
background-color: lightblue;
z-index: 20;
}
canvas {
border: 1px dotted red;
padding: 80px;
background-color: lightgray;
transform: scale(0.5);
}<!DOCTYPE html>
<html>
<head>
<link rel = "stylesheet" href = "style.css" />
</head>
<body>
<div class = "wrapper">
<div class = "slideContainer">
<input type = "range" min = "1" max = "800" value = "50" class = "slider" id = "myRange">
</div>
<canvas id = "canvas"></canvas>
</div>
<script src = "script.js"></script>
</body>
</html>


![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


После исправления синтаксической ошибки вызова requestAnimationFrame(init()) вместо requestAnimationFrame(init) обратите внимание на (), все, что осталось, это приведите value вашего HTMLInput к номеру, чтобы вы не выполняли "800" + 150, что приводит к "800150".
myRange.oninput = function() {
console.info(this.value + 150);
}<input type = "range" min = "1" max = "800" value = "50" class = "slider" id = "myRange">var canvas = document.querySelector("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.width = 1600;
canvas.height = 800;
var ct = canvas.getContext("2d");
// TODO
// 1. Make a center point
// 2. Draw lines jutting from center
// 3. Draw a line parallel to canvas bottom
// 4. Draw an adjoining item upward
// x, y
// right, down
// Nomenclature
// x0a
// coordinate type, vanishingPt#, endPtName
// Vanishing point 0
var x0 = 400;
var y0 = 400;
// Vanishing point end 0a
var x0a = 0;
var y0a = 2 * y0;
// Vanishing point end 0b
var x0b = 2 * x0;
var y0b = 2 * y0;
// Define delta
var delta = 700;
function init() {
console.info(delta, "delta");
console.info(x0b, "x0b");
console.info(y0b, "y0b");
console.info(x0, "x0");
console.info(y0, "y0");
// First Line
ct.beginPath();
ct.moveTo(x0, y0);
ct.lineTo(x0a, y0a);
ct.strokeStyle = 'red';
ct.stroke();
// Second Line
ct.beginPath();
ct.moveTo(x0, y0);
ct.lineTo(x0b, x0b);
ct.strokeStyle = 'green';
ct.stroke();
// House based on second Line
ct.beginPath();
ct.moveTo(x0b, y0b); // starting point
ct.lineTo(x0b + delta, y0b); // right x+100
ct.lineTo(x0b + delta, y0b - delta); // up y-100
ct.lineTo(x0b, y0b - delta); // left x-100
ct.lineTo(x0b, y0b); // down y+100
ct.lineTo(x0b, y0b - delta); // back up y-100
//calculate
ct.lineTo(x0, y0);
ct.lineTo(x0b + delta, y0b - delta);
ct.strokeStyle = 'blue';
ct.stroke();
}
init();
var slider = document.getElementById("myRange");
slider.oninput = function () {
// coerce to Number
delta = +this.value;
requestAnimationFrame(init); // redraw everything
}body {
background-color: lightgray;
display: flex;
justify-content: center;
align-items: center;
}
.slideContainer {
position: fixed;
right: 30px;
top: 30px;
background-color: lightblue;
z-index: 20;
}
canvas {
border: 1px dotted red;
padding: 80px;
background-color: lightgray;
transform: scale(0.5);
}<div class = "wrapper">
<div class = "slideContainer">
<input type = "range" min = "1" max = "800" value = "50" class = "slider" id = "myRange">
</div>
<canvas id = "canvas"></canvas>
</div>На ваш вопрос был дан ответ, но в вашем вопросе есть некоторые части плохой практики, на которые необходимо указать, или они будут скопированы.
oninput - это событие, управляемое движением мыши.Никогда не вызывайте функцию рендеринга или requestAnimationFrame из любых событий, которые являются результатом событий перемещения мыши. События перемещения мыши на многих устройствах могут срабатывать со скоростью, намного превышающей скорость отображения (до 1000 раз в секунду). Дисплей может отображать только 1 кадр каждые 60 секунд, рисование больше не будет видно и может проглотить батареи клиента.
Если вы вызываете requestAnimationFrame из события ввода, управляемого мышью, вы в конечном итоге ставите в очередь множество рендеров для следующего обновления дисплея, и, поскольку requestAnimationFrame пытается сбалансировать нагрузку, он может поставить в очередь рендеры для следующего кадра, таким образом, последнее обновление может быть до двух. отображать кадры поздно. Большинство кадров никогда не будут видны, а вы все равно пожираете энергию.
Используйте семафор и стандартный цикл рендеринга, который отслеживает семафор и перерисовывает только при необходимости и только один раз за кадр. (см. пример)
Если вы не трансформируете холст как часть анимации, не уменьшайте его с помощью правила CSS transform: scale(0.5); (или любого другого метода масштабирования). Производительность рендеринга зависит от пикселей в секунду, если вы вдвое меньше отображаемого холста, это означает, что вам нужно визуализировать в 4 раза больше пикселей и использовать в 4 раза больше памяти.
Вы можете выполнить масштабирование с помощью двумерного API-интерфейса Canvas, что позволит сэкономить заряд батареи клиентов и повысить производительность.
Я полностью переписал код, надеюсь, это поможет. Прокомментированы два основных момента: Обновления и Масштаб. Добавлен код для использования точек вместо координат x, y, поскольку я ленив.
requestAnimationFrame(update); // start anim loop
const ctx = canvas.getContext("2d");
const width = 1600; // The ideal resolution
const height = 800; // used to scale content
canvas.width = innerWidth;
canvas.height = innerHeight;
//Scales 2D context to always show the ideal resolution area
const scaleToFit = () => { // sets canvas scale to fit content
var scale = Math.min(canvas.width / width, canvas.height / height);
ctx.setTransform(scale, 0, 0, scale, 0, 0);
}
var redraw = true; // when true scene is redrawn ready for the next display refresh
// Working with points is easier
const point = (x = 0, y = 0) => ({x, y});
const pointCpy = (p, x = 0, y = 0) => ({x: p.x + x, y: p.y + y});
const scalePoint = (origin, point, scale) => {
point.x = (point.x - origin.x) * scale + origin.x;
point.y = (point.y - origin.y) * scale + origin.y;
};
const p1 = point(400,400);
const pA = point(p1.x, p1.y * 2);
const pB = point(p1.x * 2, p1.y * 2);
var delta = 50;
// the slider input event should not directly trigger a render
slider.addEventListener("input",(e) => {
delta = Number(e.target.value);
redraw = true; // use a semaphore to indicate content needs to redraw.
});
function update() { // this is the render loop it only draws when redraw is true
if (redraw) { // monitor semaphore
redraw = false; // clear semaphore
ctx.setTransform(1,0,0,1,0,0); // resets transform
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
scaleToFit();
draw();
}
requestAnimationFrame(update);
}
// this was your function init()
function draw() {
drawLine(p1, pA, "red");
drawLine(p1, pB, "green");
drawVBox(pB, delta, p1, "blue");
}
function drawVBox(p, size, vp, col, width) { // p is bottom left vp is vanish point
ctx.strokeStyle = col;
ctx.lineWidth = width;
const p0 = pointCpy(p); // get corners
const p1 = pointCpy(p, size);
const p2 = pointCpy(p, size, -size);
const p3 = pointCpy(p, 0, -size);
drawPoly(col, width, p0, p1, p2, p3)
ctx.beginPath(); // draw vanish lines
pathLine(p0, vp);
pathLine(p1, vp);
pathLine(p2, vp);
pathLine(p3, vp);
ctx.stroke();
const scale = 1 - size / (800 * 2);
scalePoint(vp, p0, scale);
scalePoint(vp, p1, scale);
scalePoint(vp, p2, scale);
scalePoint(vp, p3, scale);
drawPoly(col, width, p0, p1, p2, p3);
}
// Use function to do common tasks and save your self a lot of typing
function drawLine(p1, p2, col, width = 1) {
ctx.strokeStyle = col;
ctx.lineWidth = width;
ctx.beginPath();
ctx.lineTo(p1.x, p1.y); // First point after beginPath can be lineTo
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
}
function drawPoly(col,width, ...points) {
ctx.strokeStyle = col;
ctx.lineWidth = width;
ctx.beginPath();
for(const p of points){
ctx.lineTo(p.x, p.y); // First point after beginPath can be lineTo
}
ctx.closePath(); // draw closing line
ctx.stroke();
}
function pathLine(p1, p2) {
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
}canvas {
position : absolute;
top: 0px;
left: 0px;
background-color: lightgray;
z-index: -20;
}<canvas id = "canvas"></canvas>
<input type = "range" min = "1" max = "800" value = "50" id = "slider">
<code id = "info"></code>вау, большое спасибо за обзор кода :). Я все еще новичок в использовании <canvas>, так что это было очень полезно
Это, очевидно, будет зависеть от типа приложения, но для тех, которые рисуют только в ответ на пользовательские события, поддержание активного цикла rAF, вероятно, тоже не лучшее направление ;-) Вместо этого вы можете захотеть дроссель ваших обработчиков событий, поэтому что ваша вкладка может фактически не выполнять вычислений, когда с ней не взаимодействуют. Вот старая и базовая реализация дроссель на основе RF
Кадры @Kaiido вызываются как можно скорее после обновления дисплея. Как остановить рендеринг через время после кадра и до следующего обновления дисплея? Даже самый простой рендеринг ctx.clearRect(0,0,1,1) занимает на 2000 раз больше времени, чем if (redraw){ ...}, если redraw - это false. Ваш дроссель в среднем позволяет выполнять 1 дополнительный рендеринг на кадр (округление в меньшую сторону и на медленной машине) при перемещении мыши, поэтому за одну секунду событий перемещения ваш дроссель генерирует ненужные рендеры, потребляющие эквивалент получасовых вызовов незанятых кадров или 1 рендер = 33 секунды не рендеров rAF
@ Blindman67 Я думаю, вы забыли сам rAF в своих расчетах ;-) даже если внутренний код, это далеко не нуп. И я не вижу, где вы видите лишний рендеринг, троттлинг в среднем вызывает одну потерю рендеринга (мы всегда на позднем кадре)
@ Ваш дроссель допускает дополнительные вызовы rAF в промежутке времени между выходом rAF и следующим обновлением дисплея, которое может длиться до 15 + мс. Seq .. Обновление дисплея, затем событие мыши запрашивает фрейм. Фрейм вызывается сразу после выхода из события, очищает активный флаг и завершается. Затем срабатывает другое событие мыши и запрашивает другой фрейм, который вызывается после выхода из события. это два рендера менее чем за 16 мс. Как остановить рендеринг по истечении времени после кадра и до следующего обновления дисплея?
Я думаю, я понимаю, откуда взялось ваше заблуждение. Нет, rAF не выполняет обратный вызов как можно скорее, у rAF есть собственная задача в цикл событий. Сама эта задача вызывается только при необходимости. Живая демонстрация. Теперь наличие непустого список обратных вызовов анимации заставляет браузер думать, что ваша вкладка полностью активна, если она полностью не скрыта, и, таким образом, он будет запускать цикл событий с нормальной частотой плюс вводить задачи рендеринга при 60 Гц, что является не просто задачей if (false) ;-)
Если подумать еще раз, если бы он работал так, как вы думали, как бы простой fn => rAF (fn) срабатывал со скоростью 60 кадров в секунду? Если бы он был вызван немедленно в конце цикла событий, то новый rAF будет добавлен перед следующим обновлением экрана, сам вызовется немедленно и добавит новый и т. д. Точно так же, как crazyLoop в моей скрипке, однако это не так, как это работает. Кроме того, cancelAnimationFrame был бы бесполезен, если бы нам пришлось вызывать его из того же цикла событий.
@Kaiido Узнал, почему вы не видите того, что раньше происходило. Похоже, что Chrome и FF больше не запускают движения мыши со скоростью выше 1/60 (в соответствии с событиями указателя и касания). Я знаю это только сейчас благодаря эксперименту с Chrome, никогда не позволяющим более одной мыши перемещаться за кадр (между вызовами rAF), а FF очень редко пропускает второе движение мыши (после обновления дисплея и до запроса кадра, таким образом, считается 2, с предыдущим кадром считая 0) Когда это произошло, не знаю. Это объясняет, почему некоторые из моих приложений потеряли точность направления движения мыши.
@ Blindman67 нет. Прочтите мой последний комментарий. Что заставит нормальный цикл rAF ждать, но не другие? И в моей скрипке crazyLoop входит в каждый цикл событий (не измерял, но сотни + кадр), и если вы случайно нажмете на «пакет», он будет рекурсивно срабатывать в 1000 раз больше за цикл событий. Итак, мой дроссель работает :-) Это не имеет ничего общего с тем, что UA является порогом частоты событий мыши ... И в любом случае, главное, что наличие непустого списка обратных вызовов анимации помечает ваш документ как анимированный и, следовательно, активный и, следовательно, непостоянный. браузером whoch - это не просто if (false) для аккумуляторов устройства
@Kaiido Вызов rAf из контекста выполнения обратного вызова rAF заставляет следующий обратный вызов быть в следующем кадре. Вызов rAF вне контекста выполнения обратного вызова rAF или вызов rAF более одного раза в обратном вызове rAF приведет к вызову 2 или более обратных вызовов для каждого кадра. Он разработан, чтобы позволить нескольким независимым петлям rAF работать вместе со скоростью 60 кадров в секунду. И нет, нет компоновки, если нет изменений в обратных буферах (грязный флаг) (используйте инструменты разработчика, чтобы увидеть). Добавление одного события пользовательского интерфейса на страницу потребляет больше энергии, чем пустой цикл rAF.
То, что вы говорите, не имеет смысла ... Какое волшебство было бы в исполнителе обратных вызовов rAF, который сказал бы здесь, что я должен ждать здесь, я не должен. И я не сказал, что они пойдут на самом деле рисовать фрейм (какие инструменты разработки вы говорите, показывают), но они по-прежнему поддерживают весь цикл обработки событий только потому, что документ помечен как анимированный. Пожалуйста, прочтите спецификации и проверьте вкладку «Производительность» в ваших инструментах для разработки. Наличие непрерывного цикла обеспечивает непрерывную работу UA. Ничего не делает. Браузер - это не просто js-исполнитель ...
И даже если бы мы предположили, что существует такая магия, как бы вы заставили cancelAnimationFrame работать с вашей логикой? Как вы думаете, они также сохраняют состояние перед выполнением кода в обратном вызове на всякий случай, чтобы они могли волшебным образом восстановить его? Нет, правда в том, что в цикле событий есть специальная задача, которую UA могут посещать только с определенной частотой кадров, и только если они думают, что должны (.7 здесь), и что эта задача сама вызовет под- задача выполнения обратных вызовов rAF. Никакой магии.
И на самом деле, хотя они и не должны этого делать, мой Chrome 70 действительно обновляет + композитные слои каждый кадр, когда установлен rAF. jsfiddle.net/L890zep6
@Kaiido Не волшебство, это вызванный контекст выполнения, это то, как rAF знает, когда вызывать обратный вызов. cancelAnimationFrame работает так же, как отмена любого таймера, предоставление дескриптора (вы знаете, что у rAF есть возврат, почему это должно быть?). А событие мыши поддерживает события пользовательского интерфейса, что намного сложнее, чем rAF. Что касается сохраненного состояния, я понятия не имею, о чем вы говорите. Попробуйте этот (function A(){requestAnimationFrame(A);requestAnimationFrame(A)})().
Позвольте нам продолжить обсуждение в чате.
@Kaiido На скрипке !! пожалуйста!?!, код скрипта, который вы запускаете, сильно изменен, страница заполнена событиями и запущены десятки тысяч строк JS. Напишите простой HTML-документ и проверьте его.
Тем не менее вы можете видеть, что, когда цикл rAF выключен, в записях perf ничего не сообщается, и что при включении есть js exec + Composite + update.
ах теперь я понимаю.
this.valueиз слайдера представляет собой строку. Спасибо за помощь :)