|
|
|
|
/* typehints:start */
|
|
|
|
|
import { AtlasSprite } from "./sprites";
|
|
|
|
|
import { DrawParameters } from "./draw_parameters";
|
|
|
|
|
/* typehints:end */
|
|
|
|
|
|
|
|
|
|
import { Math_PI, Math_round, Math_atan2, Math_hypot, Math_floor } from "./builtins";
|
|
|
|
|
import { Vector } from "./vector";
|
|
|
|
|
import { Rectangle } from "./rectangle";
|
|
|
|
|
import { createLogger } from "./logging";
|
|
|
|
|
|
|
|
|
|
const logger = createLogger("draw_utils");
|
|
|
|
|
|
|
|
|
|
export function initDrawUtils() {
|
|
|
|
|
CanvasRenderingContext2D.prototype.beginRoundedRect = function (x, y, w, h, r) {
|
|
|
|
|
if (r < 0.05) {
|
|
|
|
|
this.beginPath();
|
|
|
|
|
this.rect(x, y, w, h);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (w < 2 * r) {
|
|
|
|
|
r = w / 2;
|
|
|
|
|
}
|
|
|
|
|
if (h < 2 * r) {
|
|
|
|
|
r = h / 2;
|
|
|
|
|
}
|
|
|
|
|
this.beginPath();
|
|
|
|
|
this.moveTo(x + r, y);
|
|
|
|
|
this.arcTo(x + w, y, x + w, y + h, r);
|
|
|
|
|
this.arcTo(x + w, y + h, x, y + h, r);
|
|
|
|
|
this.arcTo(x, y + h, x, y, r);
|
|
|
|
|
this.arcTo(x, y, x + w, y, r);
|
|
|
|
|
// this.closePath();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
CanvasRenderingContext2D.prototype.beginCircle = function (x, y, r) {
|
|
|
|
|
if (r < 0.05) {
|
|
|
|
|
this.beginPath();
|
|
|
|
|
this.rect(x, y, 1, 1);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.beginPath();
|
|
|
|
|
this.arc(x, y, r, 0, 2.0 * Math_PI);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param {object} param0
|
|
|
|
|
* @param {DrawParameters} param0.parameters
|
|
|
|
|
* @param {AtlasSprite} param0.sprite
|
|
|
|
|
* @param {number} param0.x
|
|
|
|
|
* @param {number} param0.y
|
|
|
|
|
* @param {number} param0.angle
|
|
|
|
|
* @param {number} param0.size
|
|
|
|
|
* @param {number=} param0.offsetX
|
|
|
|
|
* @param {number=} param0.offsetY
|
|
|
|
|
*/
|
|
|
|
|
export function drawRotatedSprite({ parameters, sprite, x, y, angle, size, offsetX = 0, offsetY = 0 }) {
|
|
|
|
|
parameters.context.translate(x, y);
|
|
|
|
|
parameters.context.rotate(angle);
|
|
|
|
|
sprite.drawCachedCentered(parameters, offsetX, offsetY, size, false);
|
|
|
|
|
parameters.context.rotate(-angle);
|
|
|
|
|
parameters.context.translate(-x, -y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function drawLineFast(context, { x1, x2, y1, y2, color = null, lineSize = 1 }) {
|
|
|
|
|
const dX = x2 - x1;
|
|
|
|
|
const dY = y2 - y1;
|
|
|
|
|
|
|
|
|
|
const angle = Math_atan2(dY, dX) + 0.0 * Math_PI;
|
|
|
|
|
const len = Math_hypot(dX, dY);
|
|
|
|
|
|
|
|
|
|
context.translate(x1, y1);
|
|
|
|
|
context.rotate(angle);
|
|
|
|
|
|
|
|
|
|
if (color) {
|
|
|
|
|
context.fillStyle = color;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
context.fillRect(0, -lineSize / 2, len, lineSize);
|
|
|
|
|
|
|
|
|
|
context.rotate(-angle);
|
|
|
|
|
context.translate(-x1, -y1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const INSIDE = 0;
|
|
|
|
|
const LEFT = 1;
|
|
|
|
|
const RIGHT = 2;
|
|
|
|
|
const BOTTOM = 4;
|
|
|
|
|
const TOP = 8;
|
|
|
|
|
|
|
|
|
|
// https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm
|
|
|
|
|
|
|
|
|
|
function computeOutCode(x, y, xmin, xmax, ymin, ymax) {
|
|
|
|
|
let code = INSIDE;
|
|
|
|
|
|
|
|
|
|
if (x < xmin)
|
|
|
|
|
// to the left of clip window
|
|
|
|
|
code |= LEFT;
|
|
|
|
|
else if (x > xmax)
|
|
|
|
|
// to the right of clip window
|
|
|
|
|
code |= RIGHT;
|
|
|
|
|
if (y < ymin)
|
|
|
|
|
// below the clip window
|
|
|
|
|
code |= BOTTOM;
|
|
|
|
|
else if (y > ymax)
|
|
|
|
|
// above the clip window
|
|
|
|
|
code |= TOP;
|
|
|
|
|
|
|
|
|
|
return code;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cohen–Sutherland clipping algorithm clips a line from
|
|
|
|
|
// P0 = (x0, y0) to P1 = (x1, y1) against a rectangle with
|
|
|
|
|
// diagonal from (xmin, ymin) to (xmax, ymax).
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param {CanvasRenderingContext2D} context
|
|
|
|
|
*/
|
|
|
|
|
export function drawLineFastClipped(context, rect, { x0, y0, x1, y1, color = null, lineSize = 1 }) {
|
|
|
|
|
const xmin = rect.x;
|
|
|
|
|
const ymin = rect.y;
|
|
|
|
|
const xmax = rect.right();
|
|
|
|
|
const ymax = rect.bottom();
|
|
|
|
|
|
|
|
|
|
// compute outcodes for P0, P1, and whatever point lies outside the clip rectangle
|
|
|
|
|
let outcode0 = computeOutCode(x0, y0, xmin, xmax, ymin, ymax);
|
|
|
|
|
let outcode1 = computeOutCode(x1, y1, xmin, xmax, ymin, ymax);
|
|
|
|
|
let accept = false;
|
|
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-constant-condition
|
|
|
|
|
while (true) {
|
|
|
|
|
if (!(outcode0 | outcode1)) {
|
|
|
|
|
// bitwise OR is 0: both points inside window; trivially accept and exit loop
|
|
|
|
|
accept = true;
|
|
|
|
|
break;
|
|
|
|
|
} else if (outcode0 & outcode1) {
|
|
|
|
|
// bitwise AND is not 0: both points share an outside zone (LEFT, RIGHT, TOP,
|
|
|
|
|
// or BOTTOM), so both must be outside window; exit loop (accept is false)
|
|
|
|
|
break;
|
|
|
|
|
} else {
|
|
|
|
|
// failed both tests, so calculate the line segment to clip
|
|
|
|
|
// from an outside point to an intersection with clip edge
|
|
|
|
|
let x, y;
|
|
|
|
|
|
|
|
|
|
// At least one endpoint is outside the clip rectangle; pick it.
|
|
|
|
|
let outcodeOut = outcode0 ? outcode0 : outcode1;
|
|
|
|
|
|
|
|
|
|
// Now find the intersection point;
|
|
|
|
|
// use formulas:
|
|
|
|
|
// slope = (y1 - y0) / (x1 - x0)
|
|
|
|
|
// x = x0 + (1 / slope) * (ym - y0), where ym is ymin or ymax
|
|
|
|
|
// y = y0 + slope * (xm - x0), where xm is xmin or xmax
|
|
|
|
|
// No need to worry about divide-by-zero because, in each case, the
|
|
|
|
|
// outcode bit being tested guarantees the denominator is non-zero
|
|
|
|
|
if (outcodeOut & TOP) {
|
|
|
|
|
// point is above the clip window
|
|
|
|
|
x = x0 + ((x1 - x0) * (ymax - y0)) / (y1 - y0);
|
|
|
|
|
y = ymax;
|
|
|
|
|
} else if (outcodeOut & BOTTOM) {
|
|
|
|
|
// point is below the clip window
|
|
|
|
|
x = x0 + ((x1 - x0) * (ymin - y0)) / (y1 - y0);
|
|
|
|
|
y = ymin;
|
|
|
|
|
} else if (outcodeOut & RIGHT) {
|
|
|
|
|
// point is to the right of clip window
|
|
|
|
|
y = y0 + ((y1 - y0) * (xmax - x0)) / (x1 - x0);
|
|
|
|
|
x = xmax;
|
|
|
|
|
} else if (outcodeOut & LEFT) {
|
|
|
|
|
// point is to the left of clip window
|
|
|
|
|
y = y0 + ((y1 - y0) * (xmin - x0)) / (x1 - x0);
|
|
|
|
|
x = xmin;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now we move outside point to intersection point to clip
|
|
|
|
|
// and get ready for next pass.
|
|
|
|
|
if (outcodeOut == outcode0) {
|
|
|
|
|
x0 = x;
|
|
|
|
|
y0 = y;
|
|
|
|
|
outcode0 = computeOutCode(x0, y0, xmin, xmax, ymin, ymax);
|
|
|
|
|
} else {
|
|
|
|
|
x1 = x;
|
|
|
|
|
y1 = y;
|
|
|
|
|
outcode1 = computeOutCode(x1, y1, xmin, xmax, ymin, ymax);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (accept) {
|
|
|
|
|
// Following functions are left for implementation by user based on
|
|
|
|
|
// their platform (OpenGL/graphics.h etc.)
|
|
|
|
|
// DrawRectangle(xmin, ymin, xmax, ymax);
|
|
|
|
|
// LineSegment(x0, y0, x1, y1);
|
|
|
|
|
drawLineFast(context, {
|
|
|
|
|
x1: x0,
|
|
|
|
|
y1: y0,
|
|
|
|
|
x2: x1,
|
|
|
|
|
y2: y1,
|
|
|
|
|
color,
|
|
|
|
|
lineSize,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Converts an HSL color value to RGB. Conversion formula
|
|
|
|
|
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
|
|
|
|
|
* Assumes h, s, and l are contained in the set [0, 1] and
|
|
|
|
|
* returns r, g, and b in the set [0, 255].
|
|
|
|
|
*
|
|
|
|
|
* @param {number} h The hue
|
|
|
|
|
* @param {number} s The saturation
|
|
|
|
|
* @param {number} l The lightness
|
|
|
|
|
* @return {Array} The RGB representation
|
|
|
|
|
*/
|
|
|
|
|
export function hslToRgb(h, s, l) {
|
|
|
|
|
let r;
|
|
|
|
|
let g;
|
|
|
|
|
let b;
|
|
|
|
|
|
|
|
|
|
if (s === 0) {
|
|
|
|
|
r = g = b = l; // achromatic
|
|
|
|
|
} else {
|
|
|
|
|
// tslint:disable-next-line:no-shadowed-variable
|
|
|
|
|
const hue2rgb = function (p, q, t) {
|
|
|
|
|
if (t < 0) {
|
|
|
|
|
t += 1;
|
|
|
|
|
}
|
|
|
|
|
if (t > 1) {
|
|
|
|
|
t -= 1;
|
|
|
|
|
}
|
|
|
|
|
if (t < 1 / 6) {
|
|
|
|
|
return p + (q - p) * 6 * t;
|
|
|
|
|
}
|
|
|
|
|
if (t < 1 / 2) {
|
|
|
|
|
return q;
|
|
|
|
|
}
|
|
|
|
|
if (t < 2 / 3) {
|
|
|
|
|
return p + (q - p) * (2 / 3 - t) * 6;
|
|
|
|
|
}
|
|
|
|
|
return p;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
|
|
|
let p = 2 * l - q;
|
|
|
|
|
r = hue2rgb(p, q, h + 1 / 3);
|
|
|
|
|
g = hue2rgb(p, q, h);
|
|
|
|
|
b = hue2rgb(p, q, h - 1 / 3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [Math_round(r * 255), Math_round(g * 255), Math_round(b * 255)];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function wrapText(context, text, x, y, maxWidth, lineHeight, stroke = false) {
|
|
|
|
|
var words = text.split(" ");
|
|
|
|
|
var line = "";
|
|
|
|
|
|
|
|
|
|
for (var n = 0; n < words.length; n++) {
|
|
|
|
|
var testLine = line + words[n] + " ";
|
|
|
|
|
var metrics = context.measureText(testLine);
|
|
|
|
|
var testWidth = metrics.width;
|
|
|
|
|
if (testWidth > maxWidth && n > 0) {
|
|
|
|
|
if (stroke) {
|
|
|
|
|
context.strokeText(line, x, y);
|
|
|
|
|
} else {
|
|
|
|
|
context.fillText(line, x, y);
|
|
|
|
|
}
|
|
|
|
|
line = words[n] + " ";
|
|
|
|
|
y += lineHeight;
|
|
|
|
|
} else {
|
|
|
|
|
line = testLine;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (stroke) {
|
|
|
|
|
context.strokeText(line, x, y);
|
|
|
|
|
} else {
|
|
|
|
|
context.fillText(line, x, y);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns a rotated trapez, used for spotlight culling
|
|
|
|
|
* @param {number} x
|
|
|
|
|
* @param {number} y
|
|
|
|
|
* @param {number} w
|
|
|
|
|
* @param {number} h
|
|
|
|
|
* @param {number} leftHeight
|
|
|
|
|
* @param {number} angle
|
|
|
|
|
*/
|
|
|
|
|
export function rotateTrapezRightFaced(x, y, w, h, leftHeight, angle) {
|
|
|
|
|
const halfY = y + h / 2;
|
|
|
|
|
const points = [
|
|
|
|
|
new Vector(x, halfY - leftHeight / 2),
|
|
|
|
|
new Vector(x + w, y),
|
|
|
|
|
new Vector(x, halfY + leftHeight / 2),
|
|
|
|
|
new Vector(x + w, y + h),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return Rectangle.getAroundPointsRotated(points, angle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Converts values from 0 .. 255 to values like 07, 7f, 5d etc
|
|
|
|
|
* @param {number} value
|
|
|
|
|
* @returns {string}
|
|
|
|
|
*/
|
|
|
|
|
export function mapClampedColorValueToHex(value) {
|
|
|
|
|
const hex = "0123456789abcdef";
|
|
|
|
|
return hex[Math_floor(value / 16)] + hex[value % 16];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Converts rgb to a hex string
|
|
|
|
|
* @param {number} r
|
|
|
|
|
* @param {number} g
|
|
|
|
|
* @param {number} b
|
|
|
|
|
* @returns {string}
|
|
|
|
|
*/
|
|
|
|
|
export function rgbToHex(r, g, b) {
|
|
|
|
|
return mapClampedColorValueToHex(r) + mapClampedColorValueToHex(g) + mapClampedColorValueToHex(b);
|
|
|
|
|
}
|