UNPKG

pxt-common-packages

Version:
416 lines (371 loc) 17.6 kB
namespace sprites { export class BaseSpriteSayRenderer { constructor(public text: string, public fgColor: number, public bgColor: number) { } draw(screen: Image, camera: scene.Camera, owner: Sprite) { } update(dt: number, camera: scene.Camera, owner: Sprite) { } destroy() { } } export class SpriteSayRenderer extends BaseSpriteSayRenderer { static drawSayFrame(textLeft: number, textTop: number, textWidth: number, textHeight: number, speakerX: number, speakerY: number, color: number, canvas: Image) { if (textLeft + textWidth < 0 || textTop + textHeight < 0 || textLeft > canvas.width || textTop > canvas.height) return; if (textHeight) { // Draw main rectangle canvas.fillRect( textLeft, textTop, textWidth, textHeight, color ); // Draw lines around the rectangle to give it a bubble shape canvas.fillRect( textLeft - 1, textTop + 1, 1, textHeight - 2, color ); canvas.fillRect( textLeft + textWidth, textTop + 1, 1, textHeight - 2, color ); canvas.fillRect( textLeft + 1, textTop - 1, textWidth - 2, 1, color ); canvas.fillRect( textLeft + 1, textTop + textHeight, textWidth - 2, 1, color ); // If the speaker location is within the bubble, don't draw an arrow if (speakerX > textLeft && speakerX < textLeft + textWidth && speakerY > textTop && speakerY < textTop + textHeight) return; const xDiff = Math.max( Math.abs(speakerX - textLeft), Math.abs(speakerX - (textLeft + textWidth)) ); const yDiff = Math.max( Math.abs(speakerY - textHeight), Math.abs(speakerY - (textHeight + textHeight)) ); // Draw the arrow if (xDiff > yDiff) { if (speakerX > textLeft + textWidth) { const anchorY = Math.max(Math.min(speakerY, textTop + textHeight - 4), textTop + 5); canvas.fillRect( textLeft + textWidth + 1, anchorY - 2, 1, 3, color ); canvas.fillRect( textLeft + textWidth + 2, anchorY - 1, 1, 1, color ); } else if (speakerX < textLeft) { const anchorY = Math.max(Math.min(speakerY, textTop + textHeight - 4), textTop + 5); canvas.fillRect( textLeft - 2, anchorY - 2, 1, 3, color ); canvas.fillRect( textLeft - 3, anchorY - 1, 1, 1, color ); } else if (speakerY > textTop + textHeight) { const anchorX = Math.max(Math.min(speakerX, textLeft + textWidth - 4), textLeft + 5); canvas.fillRect( anchorX - 2, textTop + textHeight + 1, 3, 1, color ); canvas.fillRect( anchorX - 1, textTop + textHeight + 2, 1, 1, color ); } else if (speakerY < textTop) { const anchorX = Math.max(Math.min(speakerX, textLeft + textWidth - 4), textLeft + 5); canvas.fillRect( anchorX - 2, textTop - 2, 3, 1, color ); canvas.fillRect( anchorX - 1, textTop - 3, 1, 1, color ); } } else { if (speakerY > textTop + textHeight) { const anchorX = Math.max(Math.min(speakerX, textLeft + textWidth - 4), textLeft + 5); canvas.fillRect( anchorX - 2, textTop + textHeight + 1, 3, 1, color ); canvas.fillRect( anchorX - 1, textTop + textHeight + 2, 1, 1, color ); } else if (speakerY < textTop) { const anchorX = Math.max(Math.min(speakerX, textLeft + textWidth - 4), textLeft + 5); canvas.fillRect( anchorX - 2, textTop - 2, 3, 1, color ); canvas.fillRect( anchorX - 1, textTop - 3, 1, 1, color ); } else if (speakerX > textLeft + textWidth) { const anchorY = Math.max(Math.min(speakerY, textTop + textHeight - 4), textTop + 5); canvas.fillRect( textLeft + textWidth + 1, anchorY - 2, 1, 3, color ); canvas.fillRect( textLeft + textWidth + 2, anchorY - 1, 1, 1, color ); } else if (speakerX < textLeft) { const anchorY = Math.max(Math.min(speakerY, textTop + textHeight - 4), textTop + 5); canvas.fillRect( textLeft - 2, anchorY - 2, 1, 3, color ); canvas.fillRect( textLeft - 3, anchorY - 1, 1, 1, color ); } } } } protected renderText: RenderText; protected animation: RenderTextAnimation; constructor(text: string, fg: number, bg: number, animated: boolean, timeOnScreen: number) { super(text, fg, bg); this.renderText = new sprites.RenderText(text, 100); if (animated) { this.animation = new sprites.RenderTextAnimation(this.renderText, 40); if (timeOnScreen >= 0) { const numberOfPauses = this.animation.numPages() + 1; const pauseTime = Math.min((timeOnScreen / (2 * numberOfPauses)) | 0, 1000); this.animation.setPauseLength(pauseTime); this.animation.setTextSpeed(this.renderText.printableCharacters() * 1000 / (timeOnScreen - pauseTime * numberOfPauses)) } this.animation.start(); } } draw(screen: Image, camera: scene.Camera, owner: Sprite) { const ox = (owner.flags & sprites.Flag.RelativeToCamera) ? 0 : camera.drawOffsetX; const oy = (owner.flags & sprites.Flag.RelativeToCamera) ? 0 : camera.drawOffsetY; const l = Math.floor(owner.left - ox); const t = Math.floor(owner.top - oy); const height = this.animation ? this.animation.currentHeight() : this.renderText.height; const width = this.animation ? this.animation.currentWidth() : this.renderText.width; const sayLeft = l + (owner.width >> 1) - (width >> 1); const sayTop = t - height - 4; if (sayLeft + width < 0 || sayTop + height < 0 || sayLeft > screen.width || sayTop > screen.height) return; SpriteSayRenderer.drawSayFrame(sayLeft, sayTop, width, height, owner.x - ox, owner.y - oy, this.bgColor, screen); if (height) { if (this.animation) { this.animation.draw(screen, sayLeft, sayTop, this.fgColor); } else { this.renderText.draw(screen, sayLeft, sayTop, this.fgColor); } } } } export class LegacySpriteSayRenderer extends BaseSpriteSayRenderer { protected sayBubbleSprite: Sprite; protected updateSay: (dt: number, camera: scene.Camera) => void; constructor(text: string, timeOnScreen: number, owner: Sprite, fg: number, bg: number) { super(text, fg, bg); const textToDisplay = console.inspect(text).split("\n").join(" "); let pixelsOffset = 0; let holdTextSeconds = 1.5; let bubblePadding = 4; let maxTextWidth = 100; let font = image.getFontForText(textToDisplay); let startX = 2; let startY = 2; let bubbleWidth = textToDisplay.length * font.charWidth + bubblePadding; let maxOffset = textToDisplay.length * font.charWidth - maxTextWidth; let bubbleOffset: number = Fx.toInt(owner._hitbox.oy); let needsRedraw = true; // sets the default scroll speed in pixels per second let speed = 45; // Calculates the speed of the scroll if scrolling is needed and a time is specified if (timeOnScreen && maxOffset > 0) { speed = (maxOffset + (2 * maxTextWidth)) / (timeOnScreen / 1000); speed = Math.max(speed, 45); holdTextSeconds = maxTextWidth / speed; holdTextSeconds = Math.min(holdTextSeconds, 1.5); } if (timeOnScreen) { timeOnScreen = timeOnScreen + game.runtime(); } if (bubbleWidth > maxTextWidth + bubblePadding) { bubbleWidth = maxTextWidth + bubblePadding; } else { maxOffset = -1; } // reuse previous sprite if possible const imgh = font.charHeight + bubblePadding; if (!this.sayBubbleSprite || this.sayBubbleSprite.width != bubbleWidth || this.sayBubbleSprite.height != imgh) { const sayImg = image.create(bubbleWidth, imgh); if (this.sayBubbleSprite) // sprite with same image size, we can reuse it this.sayBubbleSprite.setImage(sayImg); else { // needs a new sprite this.sayBubbleSprite = sprites.create(sayImg, -1); this.sayBubbleSprite.setFlag(SpriteFlag.Ghost, true); this.sayBubbleSprite.setFlag(SpriteFlag.RelativeToCamera, !!(owner.flags & sprites.Flag.RelativeToCamera)) } } this.updateSay = (dt, camera) => { // The minus 2 is how much transparent padding there is under the sayBubbleSprite this.sayBubbleSprite.y = owner.top + bubbleOffset - ((font.charHeight + bubblePadding) >> 1) - 2; this.sayBubbleSprite.x = owner.x; this.sayBubbleSprite.z = owner.z + 1; // Update box stuff as long as timeOnScreen doesn't exist or it can still be on the screen if (!timeOnScreen || timeOnScreen > game.runtime()) { // move bubble if (!owner.isOutOfScreen(camera)) { const ox = camera.offsetX; const oy = camera.offsetY; if (this.sayBubbleSprite.left - ox < 0) { this.sayBubbleSprite.left = 0; } if (this.sayBubbleSprite.right - ox > screen.width) { this.sayBubbleSprite.right = screen.width; } // If sprite bubble above the sprite gets cut off on top, place the bubble below the sprite if (this.sayBubbleSprite.top - oy < 0) { this.sayBubbleSprite.y = (this.sayBubbleSprite.y - 2 * owner.y) * -1; } } // Pauses at beginning of text for holdTextSeconds length if (holdTextSeconds > 0) { holdTextSeconds -= game.eventContext().deltaTime; // If scrolling has reached the end, start back at the beginning if (holdTextSeconds <= 0 && pixelsOffset > 0) { pixelsOffset = 0; holdTextSeconds = maxTextWidth / speed; needsRedraw = true; } } else { pixelsOffset += dt * speed; needsRedraw = true; // Pause at end of text for holdTextSeconds length if (pixelsOffset >= maxOffset) { pixelsOffset = maxOffset; holdTextSeconds = maxTextWidth / speed; } } if (needsRedraw) { needsRedraw = false; this.sayBubbleSprite.image.fill(this.bgColor); // If maxOffset is negative it won't scroll if (maxOffset < 0) { this.sayBubbleSprite.image.print(textToDisplay, startX, startY, this.fgColor, font); } else { this.sayBubbleSprite.image.print(textToDisplay, startX - pixelsOffset, startY, this.fgColor, font); } // Left side padding this.sayBubbleSprite.image.fillRect(0, 0, bubblePadding >> 1, font.charHeight + bubblePadding, this.bgColor); // Right side padding this.sayBubbleSprite.image.fillRect(bubbleWidth - (bubblePadding >> 1), 0, bubblePadding >> 1, font.charHeight + bubblePadding, this.bgColor); // Corners removed this.sayBubbleSprite.image.setPixel(0, 0, 0); this.sayBubbleSprite.image.setPixel(bubbleWidth - 1, 0, 0); this.sayBubbleSprite.image.setPixel(0, font.charHeight + bubblePadding - 1, 0); this.sayBubbleSprite.image.setPixel(bubbleWidth - 1, font.charHeight + bubblePadding - 1, 0); } } else { // If can't update because of timeOnScreen then destroy the sayBubbleSprite and reset updateSay this.updateSay = undefined; this.sayBubbleSprite.destroy(); this.sayBubbleSprite = undefined; } } this.updateSay(0, game.currentScene().camera); } update(dt: number, camera: scene.Camera, owner: Sprite) { if (!this.sayBubbleSprite) return; this.updateSay(dt, camera); if (!this.sayBubbleSprite) return; this.sayBubbleSprite.setFlag(SpriteFlag.RelativeToCamera, !!(owner.flags & SpriteFlag.RelativeToCamera)); if (owner.flags & Flag.Destroyed) this.destroy(); } destroy() { if (this.sayBubbleSprite) this.sayBubbleSprite.destroy(); this.sayBubbleSprite = undefined; } } }