js/base/MessageWindow.js
'use strict'
import DH2DObject from './DH2DObject'
import Vector3 from './Vector3'
/**
* MessageWindow class
* @access public
*/
export default class MessageWindow extends DH2DObject {
/**
* constructor
* @access public
* @constructor
*/
constructor() {
super()
this._bindedObject = null
this._bindedCnavas = null
this._bindedCamera = null
this._bindedBone = null
this._time = 0.0
this._speed = 3.0
this._mode = 0
this._x = 100
this._y = 140
this._message = ''
this._formattedMessage = []
this._lines = 0
this._lineHeight = 15
this._messageWidth = 0
this._messageHeight = 0
this._messageNumChars = 0
this._maxWidth = -1
this._offset = null
this._screenOffset = null
this._margin = 5
this._padding = 5
this._iconPadding = 5
this._icon = null
// balloon context
this._borderColor = 'black'
this._backgroundColor = 'white'
this._globalAlpha = 1
this._globalCompositeOperation = 'source-over'
this._lineCap = 'butt'
this._lineJoin = 'miter'
this._lineWidth = 1
this._miterLimit = 10
this._balloonShadowBlur = 0
this._balloonShadowColor = 'rgba(0, 0, 0, 0.0)'
this._balloonShadowOffsetX = 0
this._balloonShadowOffsetY = 0
// text context
this._textColor = 'black'
this._font = '10px sans-serif'
this._textAlign = 'start'
this._textBaseline = 'top'
this._textShadowBlur = 0
this._textShadowColor = 'rgba(0, 0, 0, 0.0)'
this._textShadowOffsetX = 0
this._textShadowOffsetY = 0
}
/**
* animate message window
* @access public
* @param {float} elapsedTime - elapsed time (seconds) from previous frame
* @returns {void}
*/
animate(elapsedTime) {
if( this._mode === this.MODE_OPEN
|| this._mode === this.MODE_STRING
|| this._mode === this.MODE_CLOSE){
this._time += this._speed * elapsedTime
}
}
/**
* render message window
* @access public
* @returns {void}
*/
render() {
//const c = this._bindedCanvas._2DContext
const padding = this._padding
const iconPadding = this._iconPadding
const vHeight = 10
const heightMax = this._messageHeight + padding * 2
const widthMax = this._messageWidth + padding * 2
let height = 0
let width = 0
let targetX = this._x
let targetY = this._y
if(this._bindedObject && this._bindedCamera){
const worldPos = new Vector3()
if(this._bindedBone){ // FIXME
const bone = this._bindedObject._model.boneHash[this._bindedBone]
worldPos.x = bone.localMatrix.m41
worldPos.y = bone.localMatrix.m42
worldPos.z = bone.localMatrix.m43
}else{
worldPos.setValue(this._bindedObject._position)
}
if(this._offset){
worldPos.add(worldPos, this._offset)
}
const windowPos = this._bindedCamera.getScreenPosition(worldPos)
this._x = this._bindedCanvas._canvasWidth * 0.5 * (1.0 + windowPos.x)
this._y = this._bindedCanvas._canvasHeight * 0.5 * (1.0 - windowPos.y)
if(this._screenOffset){
this._x += this._screenOffset.x
this._y += this._screenOffset.y
}
targetX = this._x
targetY = this._y
const left = this._x - widthMax * 0.5
const right = this._x + widthMax * 0.5
const top = this._y - heightMax
const bottom = this._y
if(widthMax > this._bindedCanvas._canvasWidth - this._margin * 2){
// size over
// FIXME
}else if(left < this._margin){
// too left
this._x += this._margin - left
}else if(right > this._bindedCanvas._canvasWidth - this._margin){
// too right
this._x -= right - this._bindedCanvas._canvasWidth + this._margin
}
if(heightMax > this._bindedCanvas._canvasHeight - this._margin * 2){
// size over
// FIXME
}else if(top < this._margin){
// too high
this._y += this._margin - top
}else if(bottom > this._bindedCanvas._canvasHeight - this._margin){
// too low
this._y -= bottom - this._bindedCanvas._canvasHeight + this._margin
}
}
if(this._mode === this.MODE_OPEN){
if(this._time >= 1.0){
this._time = 0.0
this._mode = this.MODE_STRING
height = heightMax
width = widthMax
}else{
height = heightMax * this._time
width = widthMax * this._time
}
// FIXME
this.drawBalloon(targetX, targetY + vHeight, this._x - width * 0.5, this._y - height, width, height)
}else if(this._mode === this.MODE_STRING){
let textLen = 0
if(this._time >= 1.0){
this._time = 0.0
this._mode = this.MODE_STAY
textLen = this._messageNumChars
}else{
textLen = Math.ceil(this._messageNumChars * this._time)
}
// FIXME
this.drawBalloon(targetX, targetY + vHeight, this._x - widthMax * 0.5, this._y - heightMax, widthMax, heightMax)
this._drawIconAndText(textLen)
}else if(this._mode === this.MODE_STAY){
// FIXME
this.drawBalloon(targetX, targetY + vHeight, this._x - widthMax * 0.5, this._y - heightMax, widthMax, heightMax)
this._drawIconAndText()
}else if(this._mode === this.MODE_CLOSE){
if(this._time >= 1.0){
this._time = 0.0
this._mode = this.MODE_READY
}else{
height = heightMax * (1.0 - this._time)
width = widthMax * (1.0 - this._time)
// FIXME
this.drawBalloon(targetX, targetY + vHeight, this._x - width * 0.5, this._y - height, width, height)
}
}
}
/**
* draw icon image and text
* @access private
* @param {int} len - length of text (for animation)
* @returns {void}
*/
_drawIconAndText(len) {
const c = this._bindedCanvas._2DContext
let drawLen = this._messageNumChars
if(0 < len && len < drawLen){
drawLen = len
}
const widthMax = this._messageWidth + this._padding * 2
const heightMax = this._messageHeight + this._padding * 2
const textLeft = this._x - widthMax * 0.5 + this._padding
const textTop = this._y - heightMax + this._padding
let iconLeft = 0
let iconTop = 0
let iconLines = 0
let iconTextLeft = textLeft
const iconPadding = this._iconPadding
// drawIcon
if(this._icon){
iconTop = textTop
iconLeft = textLeft
iconLines = Math.ceil((this._icon.height + iconPadding) / this._lineHeight)
iconTextLeft += this._icon.width + iconPadding
c.drawImage(this._icon, iconLeft, iconTop)
}
// drawText
this.setupTextContext()
let numChars = 0
let line = 0
let top = this._y - this._messageHeight - this._padding
while(numChars < drawLen){
let str = this._formattedMessage[line]
let strLen = str.length
if(strLen + numChars > drawLen){
strLen = drawLen - numChars
str = str.substr(0, strLen)
}
let left = textLeft
if(line < iconLines){
left = iconTextLeft
}
//c.strokeText(str, left, top)
c.fillText(str, left, top)
numChars += strLen
top += this._lineHeight
line++
}
}
/**
* set up text context
* @access public
* @returns {void}
*/
setupTextContext() {
const c = this._bindedCanvas._2DContext
c.font = this._font
c.fillStyle = this._textColor
c.textAlign = this._textAlign
c.textBaseline = this._textBaseline
c.shadowBlur = this._textShadowBlur
c.shadowColor = this._textShadowColor
c.shadowOffsetX = this._textShadowOffsetX
c.shadowOffsetY = this._textShadowOffsetY
}
/**
* set up balloon context
* @access public
* @returns {void}
*/
setupBalloonContext() {
const c = this._bindedCanvas._2DContext
c.fillStyle = this._backgroundColor
c.strokeStyle = this._borderColor
c.globalAlpha = this._globalAlpha
c.globalCompositeOperation
= this._globalCompositeOperation
c.lineCap = this._lineCap
c.lineJoin = this._lineJoin
c.lineWidth = this._lineWidth
c.shadowBlur = this._balloonShadowBlur
c.shadowColor = this._balloonShadowColor
c.shadowOffsetX = this._balloonShadowOffsetX
c.shadowOffsetY = this._balloonShadowOffsetY
}
/**
* draw balloon to given position
* @access public
* @param {float} speakerX -
* @param {float} speakerY -
* @param {float} left -
* @param {float} top -
* @param {float} width -
* @param {float} height -
* @returns {void}
*/
drawBalloon(speakerX, speakerY, left, top, width, height) {
const c = this._bindedCanvas._2DContext
const vWidth = 5
const bottom = top + height
const right = left + width
let vLeft = speakerX - vWidth
let vRight = speakerX + vWidth
const p = this._padding
const w = this._padding * 0.67
const vLeftLimit = p + this._margin
const vRightLimit = this._bindedCanvas._canvasWidth - p - this._margin
if(vLeft < vLeftLimit){
vLeft = vLeftLimit
vRight = vLeft + vWidth * 2
}else if(vRight > vRightLimit){
vRight = vRightLimit
vLeft = vRight - vWidth * 2
}
// set balloon context
this.setupBalloonContext()
c.beginPath()
c.moveTo(speakerX, speakerY)
c.lineTo(vLeft, bottom)
c.lineTo(left + p, bottom)
c.bezierCurveTo(left + w, bottom,
left, bottom - w,
left, bottom - p)
c.lineTo(left, top + p)
c.bezierCurveTo(left, top + w,
left + w, top,
left + p, top)
c.lineTo(right - p, top)
c.bezierCurveTo(right - w, top,
right, top + w,
right, top + p)
c.lineTo(right, bottom - p)
c.bezierCurveTo(right, bottom - w,
right - w, bottom,
right - p, bottom)
c.lineTo(vRight, bottom)
c.lineTo(speakerX, speakerY)
c.closePath()
c.fill()
c.stroke()
}
/**
* open message window (start animation)
* @access public
* @returns {void}
*/
open() {
this._updateMessageSize()
this._mode = this.MODE_OPEN
this._time = 0.0
}
/**
* close message window (start animation)
* @access public
* @returns {void}
*/
close() {
this._mode = this.MODE_CLOSE
this._time = 0.0
}
/**
* get context
* @access public
* @returns {CanvasRenderingContext2D} - context
*/
getContext() {
return this._bindedCanvas._2DContext
}
/**
* set context
* @access public
* @param {CanvasRenderingContext2D} context -
* @returns {void}
*/
setContext(context) {
// duplicated
//this._bindedContext = context
}
/**
* get Canvas object
* @access public
* @returns {HTMLCanvasElement} - Canvas object
*/
getCanvas() {
return this._bindedCanvas
}
/**
* set Canvas object
* @access public
* @param {HTMLCanvasElement} canvas - canvas object
* @returns {void}
*/
setCanvas(canvas) {
this._bindedCanvas = canvas
this._updateMessageSize()
}
/**
* get icon image
* @access public
* @returns {HTMLImageElement} - icon image
*/
getIcon() {
return this._icon
}
/**
* set icon image
* @access public
* @param {HTMLImageElement} icon - icon image
* @returns {void}
*/
setIcon(icon) {
this._icon = icon
this._updateMessageSize()
}
/**
* get position offset
* @access public
* @returns {Vector3} - position
*/
getOffset() {
return this._offset
}
/**
* set position offset
* @access public
* @param {float} x - X value
* @param {float} y - Y value
* @param {float} z - Z value
* @returns {void}
*/
setOffset(x, y, z) {
if(x instanceof Vector3){
this._offset = x
}else{
this._offset = new Vector3(x, y, z)
}
}
/**
* get screen offset
* @access public
* @returns {Vector3} - screen offset
*/
getScreenOffset() {
return this._screenOffset
}
/**
* set screen offset
* @access public
* @param {float} x - X value
* @param {float} y - Y value
* @param {float} z - Z value
* @returns {void}
*/
setScreenOffset(x, y, z) {
if(x instanceof Vector3){
this._screenOffset = x
}else{
this._screenOffset = new Vector3(x, y, z)
}
}
/**
* get message text
* @access public
* @returns {string} - message text
*/
getMessage() {
return this._message
}
/**
* set message text
* @access public
* @param {string} message - text to show
* @returns {void}
*/
setMessage(message = null) {
this._message = message
this._updateMessageSize()
}
/**
* get max width
* @access public
* @returns {int} - max width of message window
*/
getMaxWidth() {
return this._maxWidth
}
/**
* set max width
* @access public
* @param {int} maxWidth -
* @returns {void}
*/
setMaxWidth(maxWidth) {
this._maxWidth = maxWidth
this._updateMessageSize()
}
/**
* update message size
* @access private
* @returns {void}
*/
_updateMessageSize() {
let iconWidth = 0
let iconHeight = 0
if(this._icon){
iconWidth = this._icon.width + this._iconPadding
iconHeight = this._icon.height + this._iconPadding
}
if(this._message === null){
this._messageNumChars = 0
this._messageWidth = iconWidth
this._messageHeight = iconHeight
return
}
const mArr = this._message.split('\n')
this._formattedMessage = []
const iconLines = Math.ceil(iconHeight / this._lineHeight)
let maxLineWidth = this._maxWidth - this._padding * 2
if(this._maxWidth < 0){
maxLineWidth = 65535
}
let strMaxWidth = 0
let maxWidth = 0
let messageNumChars = 0
let line = 0
const obj = this
mArr.forEach( (str) => {
let lineStr = str
while(lineStr.length > 0){
if(line < iconLines){
maxWidth = maxLineWidth - iconWidth
}else{
maxWidth = maxLineWidth
}
const numChars = obj._getLineChars(lineStr, maxWidth)
const chars = lineStr.substr(0, numChars)
let charsWidth = obj._bindedCanvas._2DContext.measureText(chars).width
if(line < iconLines){
charsWidth += iconWidth
}
if(charsWidth > strMaxWidth){
strMaxWidth = charsWidth
}
obj._formattedMessage.push(chars)
lineStr = lineStr.substr(numChars)
messageNumChars += numChars
line++
}
})
this._messageNumChars = messageNumChars
this._messageWidth = strMaxWidth
this._messageHeight = line * this._lineHeight
if(this._messageHeight < iconHeight){
this._messageHeight = iconHeight
}
}
/**
* get text for one line
* @access private
* @param {string} str -
* @param {int} maxWidth -
* @returns {int} - string length
*/
_getLineChars(str, maxWidth) {
const c = this._bindedCanvas._2DContext
const met = c.measureText(str)
const strlen = str.length
const strWidth = met.width
if(strWidth < maxWidth){
return strlen
}
let minLen = 0
let maxLen = strlen
let newLen = strlen >> 1
let newMet = null
while(minLen < maxLen - 1){
newMet = c.measureText(str.substr(0, newLen))
if(newMet.width < maxWidth){
minLen = newLen
newLen = (newLen + maxLen) >> 1
}else{
maxLen = newLen
newLen = (newLen + minLen) >> 1
}
}
if(newMet.width > maxWidth){
return newLen - 1
}
return newLen
}
/**
* get state of message window
* @access public
* @returns {int} - 0: ready, 1: opening, 2: show string, 3: waiting, 4: closing
*/
getState() {
return this._mode
}
}
MessageWindow.prototype.MODE_READY = 0
MessageWindow.prototype.MODE_OPEN = 1
MessageWindow.prototype.MODE_STRING = 2
MessageWindow.prototype.MODE_STAY = 3
MessageWindow.prototype.MODE_CLOSE = 4