Files
pixel.horse/src/ts/graphics/graphicsUtils.ts
T
Eliot Partridge caf70a864f Revert codestyle changes (#40)
* Revert "7f7efbb94bab8574e42d942ad82d414e007b2970"

Code style changes should probably be part of a PR
2019-08-30 10:53:14 -05:00

587 lines
19 KiB
TypeScript

import {
Says, Rect, SpriteBatch, Sprite, SpriteBorder, MessageType, Entity, Point, Pony, SpriteBatchCommons,
PaletteSpriteBatch, Palette, isPartyMessage, PartyInfo, Camera, CommonPalettes, FontPalettes, PaletteManager,
isWhisperTo, isWhisper, isThinking, isPublicMessage
} from '../common/interfaces';
import { clamp, contains, intersect, toInt, hasFlag } from '../common/utils';
import { BLACK, WHITE, OUTLINE_COLOR, getMessageColor, PARTY_COLOR, MESSAGE_COLOR, FRIENDS_COLOR } from '../common/colors';
import { tileWidth, tileHeight, tileElevation, PONY_TYPE } from '../common/constants';
import {
HAlign, VAlign, TextOptions, lineBreak, drawTextAligned, measureText, drawText, drawOutlinedText
} from '../graphics/spriteFont';
import * as sprites from '../generated/sprites';
import { fontPal, fontSmallPal } from '../client/fonts';
import { getPonyChatHeight, isPony } from '../common/pony';
import { worldToScreen } from '../common/camera';
import { multiplyColor, colorToCSS } from '../common/color';
import { getTag, getTagPalette } from '../common/tags';
import { rect } from '../common/rect';
import { mockPaletteManager } from '../common/ponyInfo';
import { sortEntities, isHidden, isFriend } from '../common/entityUtils';
const baloonTaper = [
{ w: 1, y: 2 },
{ w: 1, y: 1 },
];
const roundTaper = [
{ w: 1, y: 5 },
{ w: 1, y: 3 },
{ w: 2, y: 2 },
{ w: 4, y: 1 },
];
export const commonPalettes = createCommonPalettes(mockPaletteManager);
type AnyBatch = SpriteBatch | PaletteSpriteBatch;
export function drawTaperedRect(
batch: AnyBatch, color: number, x: number, y: number, w: number, h: number, taper: { w: number; y: number; }[]
) {
x = Math.round(x) | 0;
y = Math.round(y) | 0;
w = Math.round(w) | 0;
h = Math.round(h) | 0;
let gap = 0;
for (let i = 0; i < taper.length; i++) {
const t = taper[i];
const th = h - t.y * 2;
batch.drawRect(color, x + gap, y + t.y, t.w, th);
batch.drawRect(color, x + w - gap - t.w, y + t.y, t.w, th);
gap += t.w;
}
batch.drawRect(color, x + gap, y, Math.max(0, w - gap * 2), h);
}
export function drawRectBaloon(batch: AnyBatch, color: number, x: number, y: number, w: number, h: number) {
drawTaperedRect(batch, color, x, y, w, h, baloonTaper);
}
export function drawRoundBaloon(batch: AnyBatch, color: number, x: number, y: number, w: number, h: number) {
drawTaperedRect(batch, color, x, y, w, h, roundTaper);
}
function getMessagePalette(type: MessageType, palettes: FontPalettes) {
if (type === MessageType.Supporter2) {
return palettes.supporter2;
} else if (type === MessageType.Supporter3) {
return palettes.supporter3;
} else {
return undefined;
}
}
export function drawBaloon(
batch: PaletteSpriteBatch, { message, type = MessageType.Chat, timer = 1, total = 10 }: Says,
x: number, y: number, bounds: Rect, palettes: CommonPalettes
) {
if (!fontPal)
return;
let { w, h } = measureText(message, fontPal);
w = Math.max(w, 4);
const screenPad = 8;
const availableWidth = bounds.w - screenPad * 2;
if (w > availableWidth) {
message = lineBreak(message, fontPal, availableWidth);
const size = measureText(message, fontPal);
w = size.w;
h = size.h;
}
const { dy, alpha } = calcAnimation(timer, total);
y += dy;
const nippleX = x;
const toTheLeft = Math.max(0, screenPad - x);
const toTheRight = Math.max(0, x - bounds.w + screenPad);
x = clamp(x, screenPad + w / 2 - toTheLeft, bounds.w - screenPad - w / 2 + toTheRight);
if (intersect(0, 0, bounds.w, bounds.h, x - w / 2, y - h / 2, w, h)) {
const palette = getMessagePalette(type, palettes.mainFont);
const color = palette ? WHITE : getMessageColor(type);
const options: TextOptions = {
palette: palette || palettes.mainFont.white,
emojiPalette: palettes.mainFont.emoji,
};
if (isThinking(type)) {
drawThinkingBaloon(batch, message, color, options, x, y, w, h, alpha, nippleX);
} else if (isWhisper(type) || isWhisperTo(type)) {
drawWhisperBaloon(batch, message, color, options, x, y, w, h, alpha, nippleX);
} else {
drawSpeechBaloon(batch, message, color, options, x, y, w, h, alpha, nippleX);
}
}
}
export function drawSpeechBaloon(
batch: PaletteSpriteBatch, text: string, color: number, options: TextOptions, x: number, y: number, w: number, h: number,
alpha: number, nippleX: number,
) {
const pad = 4;
const xx = x - Math.round(w / 2);
const yy = y - h;
const nipple = sprites.nipple_2.color;
nippleX = clamp(nippleX, xx + pad, xx + w - pad);
batch.globalAlpha = 0.6 * alpha;
drawRectBaloon(batch, BLACK, xx - pad, yy - pad, w + pad * 2, h + pad * 2);
batch.drawSprite(nipple, BLACK, undefined, nippleX - Math.round(nipple.w / 2), y + pad);
batch.globalAlpha = alpha;
drawText(batch, text, fontPal, color, xx, yy, options);
batch.globalAlpha = 1;
}
export function drawWhisperBaloon(
batch: PaletteSpriteBatch, text: string, color: number, options: TextOptions, x: number, y: number, w: number, h: number,
alpha: number, nippleX: number,
) {
const pad = 4;
const xx = x - Math.round(w / 2);
const yy = y - h;
const nipple = sprites.nipple_alt_2.color;
nippleX = clamp(nippleX, xx + pad, xx + w - pad);
batch.globalAlpha = 0.6 * alpha;
const left = xx - pad;
const top = yy - pad;
const width = w + pad * 2;
const height = h + pad * 2;
batch.drawRect(BLACK, left + 2, top, width - 4, 1);
batch.drawRect(BLACK, left + 1, top + 1, width - 2, 1);
batch.drawRect(BLACK, left, top + 2, width, height - 4);
batch.drawRect(BLACK, left + 1, top + height - 2, width - 2, 1);
batch.drawRect(BLACK, left + 2, top + height - 1, width - 4, 1);
const yyy = top + height + 1;
const right = left + width - 1;
for (let tx = nippleX - 7; (tx + 5) > (left + 1); tx -= 5) {
const shorten = Math.max(0, (left + 1) - tx);
batch.drawRect(BLACK, tx + shorten, yyy, 4 - shorten, 1);
}
for (let tx = nippleX + 2; tx < right; tx += 5) {
const shorten = Math.max(0, (tx + 5) - right);
batch.drawRect(BLACK, tx, yyy, Math.min(4, 5 - shorten), 1);
}
batch.drawSprite(nipple, BLACK, undefined, nippleX - 3, y + pad + 2);
batch.globalAlpha = alpha;
drawText(batch, text, fontPal, color, xx, yy, options);
batch.globalAlpha = 1;
}
export function drawThinkingBaloon(
batch: PaletteSpriteBatch, text: string, color: number, options: TextOptions, x: number, y: number, w: number, h: number,
alpha: number, nippleX: number,
) {
const padX = 6;
const padY = 4;
const xx = x - Math.round(w / 2);
const yy = y - h;
const ox = clamp(nippleX, xx, xx + w) - 1;
const oy = y + 12;
batch.globalAlpha = 0.6 * alpha;
drawRoundBaloon(batch, BLACK, xx - padX, yy - padY, w + padX * 2, h + padY * 2);
batch.drawRect(BLACK, ox, oy, 1, 1);
batch.drawRect(BLACK, ox - 1, oy - 3, 2, 2);
batch.drawRect(BLACK, ox, oy - 7, 3, 3);
batch.globalAlpha = alpha;
drawText(batch, text, fontPal, color, xx, yy, options);
batch.globalAlpha = 1;
}
export enum DrawNameFlags {
None = 0,
Party = 1,
Friend = 2,
}
function getNameColor(flags: DrawNameFlags) {
if (hasFlag(flags, DrawNameFlags.Party)) {
return PARTY_COLOR;
} else if (hasFlag(flags, DrawNameFlags.Friend)) {
return FRIENDS_COLOR;
} else {
return WHITE;
}
}
export function drawNamePlate(
batch: PaletteSpriteBatch, text: string, x: number, y: number, flags: DrawNameFlags,
palettes: CommonPalettes, tagId?: string,
) {
const tag = getTag(tagId);
const size = measureText(text, fontPal);
const xx = x - Math.round(size.w / 2);
const yy = y - size.h + 6 - (tag ? 3 : 0);
const color = getNameColor(flags);
const options = { palette: palettes.mainFont.white, emojiPalette: palettes.mainFont.emoji };
drawOutlinedText(batch, text, fontPal, color, OUTLINE_COLOR, xx, yy, options);
if (tag) {
const tagSize = measureText(tag.label, fontSmallPal);
const textX = x - Math.round(tagSize.w / 2);
const palette = getTagPalette(tag, palettes.smallFont);
drawOutlinedText(batch, tag.label, fontSmallPal, tag.color, OUTLINE_COLOR, textX, yy + 11, { palette });
}
}
export function drawBounds(batch: PaletteSpriteBatch, e: Entity, r: Rect | undefined, color: number) {
if (r) {
batch.drawRect(
color,
Math.round(e.x * tileWidth + r.x), Math.round(e.y * tileHeight + r.y),
Math.round(r.w), Math.round(r.h));
}
}
export function drawWorldBounds(batch: PaletteSpriteBatch, e: Entity, r: Rect | undefined, color: number) {
if (r) {
batch.drawRect(
color,
Math.round((e.x + r.x) * tileWidth), Math.round((e.y + r.y) * tileHeight),
Math.round(r.w * tileWidth), Math.round(r.h * tileHeight));
}
}
export function drawBoundsOutline(batch: PaletteSpriteBatch, e: Entity, r: Rect | undefined, color: number, thickness = 1) {
if (r) {
drawOutline(
batch, color,
Math.round(e.x * tileWidth + r.x), Math.round(e.y * tileHeight + r.y),
Math.round(r.w), Math.round(r.h),
thickness);
}
}
export function drawOutlineRect(batch: SpriteBatchCommons, color: number, { x, y, w, h }: Rect, thickness = 1) {
drawOutline(batch, color, x, y, w, h, thickness);
}
export function drawOutline(batch: SpriteBatchCommons, color: number, x: number, y: number, w: number, h: number, thickness = 1) {
batch.drawRect(color, x - thickness, y - thickness, w + thickness * 2, thickness); // top
batch.drawRect(color, x - thickness, y + h, w + thickness * 2, thickness); // bottom
batch.drawRect(color, x - thickness, y, thickness, h); // left
batch.drawRect(color, x + w, y, thickness, h); // right
}
export type DrawRect = (color: number, x: number, y: number, w: number, h: number) => void;
export function drawCharacter(drawRect: DrawRect, x: number, y: number, color: number, char: string) {
switch (char) {
case '0':
drawRect(color, x, y, 1, 5);
drawRect(color, x + 1, y, 1, 1);
drawRect(color, x + 1, y + 4, 1, 1);
drawRect(color, x + 2, y, 1, 5);
return 3;
case '1':
drawRect(color, x, y + 1, 1, 1);
drawRect(color, x + 1, y, 1, 4);
drawRect(color, x, y + 4, 3, 1);
return 3;
case '2':
drawRect(color, x, y, 3, 1);
drawRect(color, x, y + 2, 3, 1);
drawRect(color, x, y + 4, 3, 1);
drawRect(color, x + 2, y + 1, 1, 1);
drawRect(color, x, y + 3, 1, 1);
return 3;
case '3':
drawRect(color, x, y, 2, 1);
drawRect(color, x, y + 2, 2, 1);
drawRect(color, x, y + 4, 2, 1);
drawRect(color, x + 2, y, 1, 5);
return 3;
case '4':
drawRect(color, x, y, 1, 3);
drawRect(color, x, y + 2, 3, 1);
drawRect(color, x + 2, y, 1, 5);
return 3;
case '5':
drawRect(color, x, y, 3, 1);
drawRect(color, x, y + 2, 3, 1);
drawRect(color, x, y + 4, 3, 1);
drawRect(color, x, y + 1, 1, 1);
drawRect(color, x + 2, y + 3, 1, 1);
return 3;
case '6':
drawRect(color, x, y, 3, 1);
drawRect(color, x, y + 2, 3, 1);
drawRect(color, x, y + 4, 3, 1);
drawRect(color, x, y, 1, 5);
drawRect(color, x + 2, y + 3, 1, 1);
return 3;
case '7':
drawRect(color, x, y, 3, 1);
drawRect(color, x + 2, y + 1, 1, 4);
return 3;
case '8':
drawRect(color, x, y, 1, 5);
drawRect(color, x + 1, y, 1, 1);
drawRect(color, x + 1, y + 2, 1, 1);
drawRect(color, x + 1, y + 4, 1, 1);
drawRect(color, x + 2, y, 1, 5);
return 3;
case '9':
drawRect(color, x, y, 1, 3);
drawRect(color, x + 1, y, 1, 1);
drawRect(color, x + 1, y + 2, 1, 1);
drawRect(color, x, y + 4, 2, 1);
drawRect(color, x + 2, y, 1, 5);
return 3;
case ':':
drawRect(color, x, y + 1, 1, 1);
drawRect(color, x, y + 3, 1, 1);
return 1;
case '.':
drawRect(color, x, y + 4, 1, 1);
return 1;
case '-':
drawRect(color, x, y + 2, 3, 1);
return 3;
case ' ':
return 2;
default:
drawRect(color, x, y, 3, 5);
return 3;
}
}
export function drawPixelTextBase(drawRect: DrawRect, x: number, y: number, color: number, text: string) {
for (let i = 0; i < text.length; i++) {
x += drawCharacter(drawRect, x, y, color, text.charAt(i)) + 1;
}
}
export function drawPixelText(batch: PaletteSpriteBatch, x: number, y: number, color: number, text: string) {
drawPixelTextBase(batch.drawRect.bind(batch), x, y, color, text);
}
export function fillRect(context: CanvasRenderingContext2D, color: string, x: number, y: number, w: number, h: number) {
context.fillStyle = color;
context.fillRect(x, y, w, h);
}
export function drawPixelTextOnCanvas(context: CanvasRenderingContext2D, x: number, y: number, color: number, text: string) {
drawPixelTextBase((color, x, y, w, h) => fillRect(context, colorToCSS(color), x, y, w, h), x, y, color, text);
}
interface Say {
message: Says;
x: number;
y: number;
}
export function compareSays(a: Say, b: Say) {
return a.message.created - b.message.created;
}
function isPartyMember(entity: Entity, party: PartyInfo | undefined) {
return entity.type === PONY_TYPE && party !== undefined && party.members.some(p => p.id === entity.id && !p.pending);
}
export function drawNames(
batch: PaletteSpriteBatch, entities: Entity[], player: Pony | undefined, party: PartyInfo | undefined,
camera: Camera, hover: Point, drawHidden: boolean, palettes: CommonPalettes
) {
sortEntities(entities);
for (const e of entities) {
if ((!isHidden(e) || drawHidden) && e.name && e !== player) {
const nameOffsetBase = 12;
const bounds = e.interactBounds || e.bounds;
const chatBounds = e.chatBounds || bounds;
if (chatBounds !== undefined && bounds !== undefined && contains(e.x, e.y, bounds, hover)) {
const { x, y } = worldToScreen(camera, e);
const nameOffset = nameOffsetBase - getChatHeight(e);
const tag = (isHidden(e) && drawHidden) ? 'hidden' : e.tag;
const flags = DrawNameFlags.None |
(isPartyMember(e, party) ? DrawNameFlags.Party : 0) |
(isFriend(e) ? DrawNameFlags.Friend : 0);
drawNamePlate(batch, e.name, x, y + chatBounds.y - nameOffset, flags, palettes, tag);
}
}
}
}
export function getChatBallonXY(e: Entity, camera: Camera): Point {
const nameOffsetBase = 12;
const bounds = e.interactBounds || e.bounds;
const chatBounds = e.chatBounds || bounds;
const screen = worldToScreen(camera, e);
const nameOffset = nameOffsetBase - getChatHeight(e);
const offset = (nameOffset + 6) + (e.tag ? 5 : 0);
const yy = screen.y + (chatBounds ? chatBounds.y : 0) - offset;
const x = screen.x + toInt(e.chatX);
const y = yy + toInt(e.chatY);
return { x, y };
}
function drawChatBaloon(batch: PaletteSpriteBatch, entity: Entity, camera: Camera, palettes: CommonPalettes) {
const { x, y } = getChatBallonXY(entity, camera);
drawBaloon(batch, entity.says!, x, y, camera, palettes);
}
export function drawChat(
batch: PaletteSpriteBatch, entities: Entity[], camera: Camera, drawHidden: boolean, palettes: CommonPalettes,
hidePublic: boolean
) {
sortEntities(entities);
for (const entity of entities) {
if ((!isHidden(entity) || drawHidden) && !isPartyMessage(entity.says!.type || MessageType.Chat)) {
if (!hidePublic || !isPublicMessage(entity.says!.type || MessageType.Chat)) {
drawChatBaloon(batch, entity, camera, palettes);
}
}
}
for (const entity of entities) {
if ((!isHidden(entity) || drawHidden) && isPartyMessage(entity.says!.type || MessageType.Chat)) {
drawChatBaloon(batch, entity, camera, palettes);
}
}
}
function getChatHeight(entity: Entity): number {
return isPony(entity) ? getPonyChatHeight(entity) : 0;
}
export const chatAnimationDuration = 0.2;
export function dismissSays(says: Says) {
if (says.timer !== undefined) {
says.timer = Math.min(says.timer, chatAnimationDuration);
}
}
function calcAnimation(timer: number, total: number) {
const start = (total - timer) / chatAnimationDuration;
const end = timer / chatAnimationDuration;
const dys = [3, 2, 1, 0, -1, 0];
const dys2 = [-4, -3, -2, -1];
const dyd = start * dys.length;
const dyd2 = end * dys2.length;
const dyi = clamp(Math.round(dyd), 0, dys.length - 1);
const dyi2 = clamp(Math.round(dyd2), 0, dys2.length);
const dy = dyi2 < dys2.length ? dys2[dyi2] : dys[dyi];
const alpha = Math.min(start, end, 1);
return { alpha, dy };
}
export function drawBox(
batch: SpriteBatchCommons, color: number, shadowColor: number, x: number, y: number, z: number,
w: number, l: number, h: number
) {
const darker = multiplyColor(color, 0.8);
const left = (x - w / 2) * tileWidth;
const bottom = y * tileHeight;
const elevation = z * tileElevation;
const width = w * tileWidth;
const frontHeight = h * tileElevation;
const topHeight = l * tileHeight;
batch.drawRect(shadowColor, left, bottom - topHeight, width, topHeight); // shadow
batch.drawRect(darker, left, bottom - elevation - frontHeight, width, frontHeight); // front
batch.drawRect(color, left, bottom - elevation - frontHeight - topHeight, width, topHeight); // top
}
export function drawSpriteBorder(
batch: SpriteBatch, border: SpriteBorder, color: number, x: number, y: number, w: number, h: number
) {
x = Math.round(x) | 0;
y = Math.round(y) | 0;
w = Math.round(w) | 0;
h = Math.round(h) | 0;
const size = border.border;
const right = x + w - size;
const bottom = y + h - size;
const bgWidth = w - size * 2;
const bgHeight = h - size * 2;
batch.drawSprite(border.topLeft, color, x, y);
batch.drawSprite(border.topRight, color, right, y);
batch.drawSprite(border.bottomLeft, color, x, bottom);
batch.drawSprite(border.bottomRight, color, right, bottom);
drawStretched(batch, border.top, color, x + size, y, bgWidth, size);
drawStretched(batch, border.left, color, x, y + size, size, bgHeight);
//drawStretched(batch, border.bg, color, x + size, y + size, bgWidth, bgHeight);
batch.drawRect(color, x + size, y + size, bgWidth, bgHeight);
drawStretched(batch, border.right, color, x + w - size, y + size, size, bgHeight);
drawStretched(batch, border.bottom, color, x + size, y + h - size, bgWidth, size);
}
function drawStretched(batch: SpriteBatch, sprite: Sprite, color: number, x: number, y: number, w: number, h: number) {
if (sprite.h && sprite.w) {
const sw = Math.min(sprite.w, w);
const sh = Math.min(sprite.h, h);
batch.drawImage(color, sprite.x, sprite.y, sw, sh, x, y, w, h);
}
}
export function drawSpriteCropped(
batch: PaletteSpriteBatch, s: Sprite, color: number, palette: Palette | undefined, x: number, y: number,
maxY: number
) {
const top = y + s.oy;
const bottom = Math.min(top + s.h, maxY);
if (bottom > top) {
batch.drawImage(s.type, color, palette, s.x, s.y, s.w, s.h, x + s.ox, top, s.w, bottom - top);
}
}
export function drawFullScreenMessage(batch: PaletteSpriteBatch, camera: Camera, text: string, palette: Palette) {
const messageHeight = 100;
const messageY = Math.round((camera.h - messageHeight) / 2);
batch.drawRect(MESSAGE_COLOR, 0, messageY, camera.w, messageHeight);
const textRect = rect(0, messageY, camera.w, messageHeight);
drawTextAligned(batch, text, fontPal, WHITE, textRect, HAlign.Center, VAlign.Middle, { palette });
}
export function createCommonPalettes(paletteManager: PaletteManager): CommonPalettes {
return {
defaultPalette: paletteManager.addArray(sprites.defaultPalette),
mainFont: {
emoji: paletteManager.addArray(sprites.emojiPalette),
white: paletteManager.addArray(sprites.fontPalette),
supporter1: paletteManager.addArray(sprites.fontSupporter1Palette),
supporter2: paletteManager.addArray(sprites.fontSupporter2Palette),
supporter3: paletteManager.addArray(sprites.fontSupporter3Palette),
},
smallFont: {
emoji: paletteManager.addArray(sprites.emojiPalette),
white: paletteManager.addArray(sprites.fontSmallPalette),
supporter1: paletteManager.addArray(sprites.fontSmallSupporter1Palette),
supporter2: paletteManager.addArray(sprites.fontSmallSupporter2Palette),
supporter3: paletteManager.addArray(sprites.fontSmallSupporter3Palette),
},
};
}