Full Code of zsxsoft/danmu-client for AI

electron e8444b64899d cached
24 files
39.7 KB
13.0k tokens
56 symbols
1 requests
Download .txt
Repository: zsxsoft/danmu-client
Branch: electron
Commit: e8444b64899d
Files: 24
Total size: 39.7 KB

Directory structure:
gitextract_7g7r0epy/

├── .eslintignore
├── .eslintrc
├── .gitattributes
├── .gitignore
├── .travis.yml
├── app/
│   ├── index.html
│   ├── index.js
│   ├── lib/
│   │   ├── danmu/
│   │   │   ├── comment.js
│   │   │   ├── commentframe.js
│   │   │   ├── frame.js
│   │   │   ├── index.js
│   │   │   ├── player.js
│   │   │   └── sprite.js
│   │   ├── listener/
│   │   │   └── index.js
│   │   └── utils/
│   │       └── index.js
│   ├── main.js
│   ├── package.json
│   ├── panel.html
│   └── panel.js
├── appveyor.yml
├── config.js
├── package.json
├── readme.md
└── resources/
    └── icon.icns

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintignore
================================================
/http/res/*
config.js

================================================
FILE: .eslintrc
================================================
{
  "root": true,
  "parser": "babel-eslint",
  "parserOptions": {
    "sourceType": "module"
  },
  "env": {
    "node": true,
    "browser": true
  },
  "extends": "standard",
  "plugins": [
    "import"
  ],
  "globals": {
    "coordinator": true,
    "windows": true
  }
}


================================================
FILE: .gitattributes
================================================
# Auto detect text files and perform LF normalization
* text=auto

# Custom for Visual Studio
*.cs     diff=csharp

# Standard to msysgit
*.doc	 diff=astextplain
*.DOC	 diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot  diff=astextplain
*.DOT  diff=astextplain
*.pdf  diff=astextplain
*.PDF	 diff=astextplain
*.rtf	 diff=astextplain
*.RTF	 diff=astextplain


================================================
FILE: .gitignore
================================================
/cache
/builds
/typings
/out
/dist

# Logs
logs
*.log

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directory
# Commenting this out is preferred by some people, see
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
node_modules

# Users Environment Variables
.lock-wscript

# =========================
# Operating System Files
# =========================

# OSX
# =========================

.DS_Store
.AppleDouble
.LSOverride

# Thumbnails
._*

# Files that might appear on external disk
.Spotlight-V100
.Trashes

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

# Windows
# =========================

# Windows image file caches
Thumbs.db
ehthumbs.db

# Folder config file
Desktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# Windows Installer files
*.cab
*.msi
*.msm
*.msp

# Windows shortcuts
*.lnk



================================================
FILE: .travis.yml
================================================
osx_image: xcode8.3
sudo: required
dist: trusty
language: node_js
node_js: '8'
env:
  global:
  - ELECTRON_CACHE=$HOME/.cache/electron
  - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
matrix:
  include:
  - os: osx
  - os: linux
cache:
  directories:
  - node_modules
  - "$HOME/.cache/electron"
  - "$HOME/.cache/electron-builder"
  - "$HOME/.npm/_prebuilds"

script:
- npm run release


================================================
FILE: app/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>DANMU Client</title>
    <style type="text/css">
	canvas{
		/**
		 * In Electron, if an dragable object covered the border, 
		 * then the window will not resizable.
		 */
		height: calc(100% - 2px);
		width: calc(100% - 2px);
		top: 1px;
		left: 1px;
		position: absolute;
	}
    
    #message {
        color: white;
        font-size: 12px;
    }
    
    .border {
        border: 3px solid red;
        height: calc(100% - 6px);
        width: calc(100% - 6px);
        top: 0;
        position: absolute;
        left: 0;
    }
    
    html {
        overflow: hidden;
    }
    </style>
</head>

<body style="background: black">
    <div class="border"></div>
    <p id="message">
        大小和位置调整完毕后,按下回车或点击按钮开启弹幕。
        <br/> 实际位置即黑框所在位置。
        <br/>
        <br/> 版本号:<span id="client-version"></span>
        <br/> 弹幕客户端制作:<span id="client-author"></span>
        <br/> GitHub: <span id="client-homepage"></span>
        <br/>
        <br/>
        <br/> 按下F1,打开GitHub,获取最新的源代码及获得帮助。
        <br/> 按下F12,打开调试器。
    </p>
    <canvas id="main-canvas" style="-webkit-app-region: drag"></canvas>
</body>
<script src="index.js"></script>

</html>


================================================
FILE: app/index.js
================================================
'use strict';
(() => {
  const electron = require('electron')
  const windows = electron.remote.getGlobal('windows')
  //    const coordinator = electron.remote.getGlobal('coordinator');
  const {shell} = electron
  const path = require('path')
  const fs = require('fs')
  const danmu = require('./lib/danmu')
  const listener = require('./lib/listener')

  const crypto = require('crypto')
  const packageJson = require('./package.json')
  let isStart = false

  let config = null
  try {
    config = eval(fs.readFileSync(path.resolve('config.js'), 'utf-8')) // eslint-disable-line no-eval
  } catch (e) {
    alert('你的config.js修改有误,解析出错:\n' + e.stack.toString())
    windows.mainWindow.openDevTools({detach: true})
    throw e
  }

  global.config = config

  function initFunction () {
    windows.mainWindow.setResizable(false) // Electron doesn't support both resizable and transparency
    windows.mainWindow.setIgnoreMouseEvents(true)

    document.getElementById('message').remove()
    document.querySelector('.border').remove()
    document.querySelector('body').style.background = 'transparent'

    listener.init(config)
    danmu.init(config, listener, document.getElementById('main-canvas'))

    isStart = true
  }

  function keydownFunction (e) {
    switch (e.keyCode) {
      case 13:
        if (!isStart) {
          initFunction()
          isStart = true
        }
        break
      case 116:
        e.preventDefault()
        break
      case 112:
        shell.openExternal(packageJson.homepage)
        break
      case 123:
        windows.mainWindow.webContents.openDevTools({detach: true})
        break
    }
  }

  document.getElementById('client-version').innerHTML = packageJson.version
  document.getElementById('client-homepage').innerHTML = packageJson.homepage
  document.getElementById('client-author').innerHTML = packageJson.author
  document.title = 'DANMU Client - Client ID = ' + crypto.createHash('md5').update(Math.random().toString()).digest('hex')
  window.addEventListener('keydown', keydownFunction, true)
})()


================================================
FILE: app/lib/danmu/comment.js
================================================
/* global config */
'use strict'

let utils = require('../utils')
let Sprite = require('./sprite')
const DW_TEXT = 1
const DW_IMAGE = 2
/**
 * 字幕对象,继承自Sprite对象
 * @param param = {id, x, y, width, height, speed, text, lifeTime, color, font}
 */
class Comment extends Sprite {
  constructor (param) {
    super(param.id, param.x, param.y, param.width, param.height, param.speed, param.lifeTime)
    this.cache = null
    this.text = param.text || '' // 文字内容
    this.lifeTime = param.lifeTime || config.display.comment.lifeTime
    this.color = param.color || config.display.comment.color
    this.font = param.font || config.display.comment.fontStyle
  }

  /**
     * 弹幕的绘制方法
     */
  draw (canvasContext) {
    canvasContext.fillStyle = this.color
    canvasContext.font = this.font
    if (!config.display.image) {
      canvasContext.shadowOffsetX = 1
      canvasContext.shadowOffsetY = 1
      canvasContext.shadowBlur = 1
      canvasContext.fillText(this.text, this.x, this.y + this.height)
      return
    }

    // 以下为图文混排相关代码
    if (this.cache === null) {
      this.cache = []
      let lastIndex = 0 // 记录未写入文字的起始点
      let nextPos = 0
      let spaceWidth = canvasContext.measureText(' ').width // 得到字间距
      utils.imageAnalyzer.regex = config.image.regex

      utils.imageAnalyzer.test((ret, imageObject) => {
        let text = this.text.substr(lastIndex, ret.index - lastIndex)
        this.cache.push({
          type: DW_TEXT,
          position: nextPos,
          text: text
        })
        nextPos += canvasContext.measureText(text).width + spaceWidth // 计算此时图片位置
        this.cache.push({
          type: DW_IMAGE,
          object: imageObject,
          position: nextPos,
          width: imageObject.width
        })
        nextPos += imageObject.width + spaceWidth
        lastIndex = ret.index + ret[0].length // 更新未写入文字的起始点
      }, this.text)
      this.cache.push({
        type: DW_TEXT,
        position: nextPos,
        text: this.text.substr(lastIndex, this.text.length)
      })
    }

    let x = this.x
    let y = this.y
    let height = this.height
    let actualHeight = y + height
    this.cache.forEach(function (val) {
      switch (val.type) {
        case DW_TEXT:
          canvasContext.shadowOffsetX = 1
          canvasContext.shadowOffsetY = 1
          canvasContext.shadowBlur = 1
          canvasContext.fillText(val.text, x + val.position, actualHeight)
          break
        case DW_IMAGE:
          if (val.object.error) {
            return // 出错
          }
          canvasContext.shadowBlur = 0
          canvasContext.shadowOffsetX = 0
          canvasContext.shadowOffsetY = 0
          if (val.object.loaded) {
            canvasContext.drawImage(val.object.element, x + val.position, y + height / 10 + 1, val.width, height) // 绘制图片
                        // 10+1是一个修正偏移的魔法数字
            val.object.loaded = true
          } else {
            utils.tryCatch(() => {
              canvasContext.drawImage(val.object.element, x + val.position, y + height / 10 + 1, val.width, height) // 绘制图片
            }, () => {
              val.object.error = true
            })
          }

          break
      }
    })
  }
  /**
     * 更新弹幕的生命状态
     */
  updateLifeTime () {
    this.lifeTime-- // 每刷新一帧,存活时间-1
    this.alive = (this.lifeTime >= 0)
  };
}

module.exports = Comment


================================================
FILE: app/lib/danmu/commentframe.js
================================================
/* global config */
'use strict'
let Frame = require('./frame')
let Comment = require('./comment')

/**
 * 弹幕frame对象,继承字Frame对象
 * @param width
 * @param height
 * @param canvasContext
 * @returns
 */
class CommentFrame extends Frame {
  constructor (width, height, canvasContext) {
    super(width, height, canvasContext)

    /**
         * 标志本帧所绘制的弹幕是可见还是隐藏
         */
    this.visible = true
    /**
         * 保存需要绘制的多层弹幕
         */
    this.layers = []
    /**
         * 弹幕ID所对应的弹幕层
         */
    this.idMap = {}
    /**
         * 创建缓冲画布
         */
    this.bufCanvas = window.document.createElement('canvas')
    this.bufCanvas.width = width
    this.bufCanvas.height = height
    /**
         * 标识是否打开弹幕
         */
    this.danmuState = true
  }

  /**
     * 重设置本frame的宽和高,同时bufCanvas尺寸也随之修改
     * @param width
     * @param height
     */
  resize (width, height) {
    this.width = width
    this.height = height

    this.bufCanvas.width = width
    this.bufCanvas.height = height
  };

  /**
     * @Override
     * 向Frame中添加弹幕对象.
     * 在本Frame中根据生成弹幕Sprite.这样当播放器进度seek的时候,重新根据弹幕信息重新new的Sprite对象的x位置是对的;
     * 如果在Playr中new好了,再根据时间add进来,当播放器往回seek的时候,加进来的是已存在的对象,这时对象的x可能是已经超出显示范围的负数了,不会被再次显示。
     */
  addSprite (info) {
    if (!this.danmuState) return

    let that = this

    let style = info.style || config.display.comment.animationStyle
    let color = info.color || config.display.comment.fontColor
    let font = info.font || config.display.comment.fontStyle
    let lifeTime = info.lifeTime || config.display.comment.lifeTime
    let height = info.height || config.display.comment.height
    let text = info.text

    // 文字的宽度【注意,ctx.measureText(text).width得到文字宽度是基于ctx当前的font的,如果取得width后改变了ctx.font很可能width与实际文字宽度不匹配】
    this.bufCanvas.getContext('2d').font = font
    let width = this.bufCanvas.getContext('2d').measureText(text).width

    let result = generateY(style, height, 0) // 计算弹幕位置,从第0层弹幕开始
    let y = result.y
    let index = result.index

    let x = generateX(style, width)

    this.layers[index].push(new Comment({
      x: x,
      y: y,
      width: width,
      height: height,
      speed: generateSpeed(style, x, y, lifeTime),
      text: text,
      lifeTime: lifeTime,
      color: color,
      font: font,
      id: info.id
    }))
    this.idMap[info.id] = {
      layer: index,
      index: this.layers[index].length - 1
    }

    /**
         * 确定弹幕的速度
         * @param style 弹幕类型
         * @returns speed{}
         */
    function generateSpeed (style, x, y, lifeTime) {
      if (style === 'scroll') {
        return {
          x: -(x + width) / lifeTime, // -(移动距离+文本宽度)/(移动时间*帧数)
          y: 0
        }
      } else if (style === 'reversescroll') {
        return {
          x: (width + that.width) / lifeTime,
          y: 0
        }
      } else if (style === 'staticdown' || style === 'staticup') {
        return {
          x: 0,
          y: 0
        }
      }
      // 这里一大串if和elseif实在是不够优雅
      // 不过目前就这四个的话懒得再去抽象成接口
      // 这里用桥接模式的话应该很合适呢
    }

    /**
         * 确定弹幕的X坐标
         * @param style 弹幕类型
         * @param textWidth 该弹幕的文字内容宽度
         * @returns x
         */
    function generateX (style, textWidth) {
      if (style === 'scroll') {
        return that.width
      } else if (style === 'reversescroll') {
        return 0
      } else if (style === 'staticdown' || style === 'staticup') {
        return (that.width - textWidth) / 2
      }
    }

    /**
         * 检查是否与当前Frame中其他弹幕重叠
         * @param y 本弹幕y坐标
         * @param size 本弹幕高度
         * @param index 当前排序所在的弹幕层
         * @returns {Boolean} true表示有重叠
         */
    function checkDanmu (y, size, index) {
      let currentLayerDanmus = that.layers[index] // 取得当前弹幕层的所有danmus
      for (let i = 0; i < currentLayerDanmus.length; i++) {
        let danmu = currentLayerDanmus[i]
        if (y + size > danmu.y && y < danmu.y + danmu.height) { // 如果有重叠
          return true
        }
      }
      return false // 没有重叠
    }
    /**
         * 确定弹幕的y坐标
         * @param style 弹幕类型
         * @param size 该弹幕的高(字号)
         * @param index 当前排序所在的弹幕层
         * @returns {} {'y坐标':y,'所在弹幕层号index':index}
         */
    function generateY (style, size, index) {
      if (index > 20) {
        return {
          'y': 0,
          'index': index - 1
        }
      } // 超过20层就不显示了

      while (!that.layers[index]) { // 如果当前弹幕层还不存在
        // 增加弹幕层
        that.layers.push([])
      }

      let y = 0
      if (style === 'scroll' || style === 'reversescroll') { // 滚动字幕尽量向顶部聚集,但不重叠
        while (y < that.height - size) {
          if (checkDanmu(y, size, index)) {
            y++
          } else { // 找到合适位置
            return {
              'y': y,
              'index': index
            }
          }
        }
      } else if (style === 'staticdown') { // 底部字幕尽量向底部聚集,但不重叠
        y = that.height - height - 8 // 从底部-文字高度-底部边距的位置开始往上排,默认底部边距是8.
        while (y > 0) {
          if (checkDanmu(y, size, index)) {
            y--
          } else { // 找到合适位置
            return {
              'y': y,
              'index': index
            }
          }
        }
      } else if (style === 'staticup') {
        y = 0 + height + 8 // 注释参见staticdown
        while (y > 0) {
          if (checkDanmu(y, size, index)) {
            y++
          } else { // 找到合适位置
            return {
              'y': y,
              'index': index
            }
          }
        }
      }

      // 没有合适位置,再次调用本方法
      return generateY(style, size, index + 1)
    }
  };

  /**
     * 向Frame中添加自定义弹幕对象.默认渲染在现有层的最后一层即最上层.
     * @param className 自定义弹幕类
     */
  addCustomSprite (info) {
    if (!this.danmuState) return

    let script = window.document.createElement('script')
    let className = 'danmuClass__' + new Date().getTime().toString() + '_' + Math.floor((Math.random() * 100000)).toString()
    let sourceCode = 'var ' + className + ' = ((function() { ' + info.sourceCode + '})()); '
    script.innerHTML = sourceCode
    window.document.head.appendChild(script)
    let customSprite = new (window[className])(info)

    // 高度、宽度等不受系统管理
    while (!this.layers[this.layers.length - 1]) { // 如果当前弹幕层还不存在
      this.layers.push([]) // 增加弹幕层
    }
    let layer = this.layers.length - 1
    this.layers[layer].push(customSprite)
    this.idMap[info.id] = {
      layer: layer,
      index: this.layers[layer].length - 1
    }
  };

  /**
     * @Override
     * 对本帧进行分层渲染
     */
  render () {
    let bufCanvasCtx = this.bufCanvas.getContext('2d')
    this.ctx.clearRect(0, 0, this.width, this.height) // 清空结果画布
    bufCanvasCtx.clearRect(0, 0, this.width, this.height) // 清空buffer画布
    // 渲染各层精灵到buffer画布上
    for (let i = 0; i < this.layers.length; i++) {
      for (let j = 0; j < this.layers[i].length; j++) {
        this.layers[i][j].draw(bufCanvasCtx)
      }
    }
    // 往主图层上绘制buffer图层
    this.ctx.drawImage(this.bufCanvas, 0, 0)
  };

  /**
     * 如果播放器seek了,清空所有弹幕
     */
  clearDanmu () {
    for (let i = 0; i < this.layers.length; i++) {
      delete this.layers[i] // 删除相应对象
    }
    Object.keys(this.idMap, key => delete this.idMap[key])
    this.idMap = {}
    this.layers = []
  };

  /**
     * 停止显示弹幕,停止创建弹幕
     */
  stopDanmu () {
    this.clearDanmu()
    this.danmuState = false
  };

  /**
     * 重新打开弹幕功能
     */
  restartDanmu () {
    this.danmuState = true
  };
  /**
     * @Override
     * 更新CommentFrame中弹幕Sprite的状态
     */
  updateSprite () {
    for (let i = 0; i < this.layers.length; i++) {
      for (let j = 0; j < this.layers[i].length; j++) {
        // 更新位置
        this.layers[i][j].move()
        // 更新生命状态
        this.layers[i][j].updateLifeTime()
      }
    }
  };
  /**
     * @Override
     * 清除已经死亡的Sprite
     */
  clearSprite () {
    for (let i = 0; i < this.layers.length; i++) {
      for (let j = 0; j < this.layers[i].length; j++) {
        if (!this.layers[i][j].alive) {
          delete this.idMap[this.layers[i][j].id]
          delete this.layers[i][j] // 删除相应对象
          this.layers[i] = this.layers[i].slice(0, j).concat(this.layers[i].slice(j + 1, this.layers[i].length)) // 清除数组中该位置
          for (let l = j; l < this.layers[i].length; l++) {
            this.idMap[this.layers[i][l].id].index--
          }
        }
      }
    }
  };

  /**
     * @Override
     * 删除精灵元素
     * @param sprite
     */
  deleteSprites (indexs) {
    indexs.forEach(index => {
      if (this.idMap[index]) {
        if (this.layers[this.idMap[index].layer]) {
          if (this.layers[this.idMap[index].layer][this.idMap[index].index]) {
            this.layers[this.idMap[index].layer][this.idMap[index].index].remove()
          }
        }
      }
    })
  };
}
module.exports = CommentFrame


================================================
FILE: app/lib/danmu/frame.js
================================================
'use strict'

/**
 * 帧对象
 * 原理就是每到一定时间就清除canvas,然后调用当前帧里的所有的元素的draw()方法。将所有动画元素按照新的配置重画,从而生成动画。
 * 之后程序无需关心元素的重画,只需要调整元素属性即可。
 */
class Frame {
  constructor (width, height, canvasContext) {
    /**
     * 帧的宽和高
     */
    this.width = width
    this.height = height
    /**
     * 记录绘制Frame的定时器id
     */
    this.renderTimer = null
    /**
     * 本帧所要绘制的精灵元素
     */
    this.sprites = []
    /**
     * 保存本帧相关的canvas标签的context
     */
    this.ctx = canvasContext
    /**
     * 记录FPS
     */
    this.fps = 0
  }

  begin () {
    if (this.renderTimer !== null) {
      return // 防止重复启动
    }

    let that = this;
    (function animate () {
      that.updateSprite() // 更新Sprite
      that.clearSprite() // 清除无效Sprite
      that.render()
      that.fps = that.countFps() // 计算FPS
      that.renderTimer = window.requestAnimationFrame(animate, that)
    })()
  };

  /**
     * 渲染本帧【可根据需要在子对象中复写此方法】
     */
  render () {
    this.ctx.clearRect(0, 0, this.width, this.height)
    for (let i = 0; i < this.sprites.length; i++) {
      this.sprites[i].draw(this.ctx)
    }
  };

  /**
     * 停止动画
     */
  stop () {
    if (this.renderTimer === null) {
      return
    }
    window.cancelAnimationFrame(this.renderTimer)
    this.renderTimer = null
  };

  /**
     * 添加精灵元素
     * @param sprite
     */
  addSprite (sprite) {
    this.sprites.push(sprite)
  };
  /**
     * 删除精灵元素
     * @param sprite
     */
  deleteSprites (indexs) {
    let that = this
    indexs.forEach(function (index) {
      if (that.sprites[index]) {
        that.sprites[index].remove()
        that.sprites.splice(index, 1)
      }
    })
  };

  /**
     * 更新本frame下所有Sprite的位置
     */
  updateSprite () {
    this.sprites.forEach(sprite => {
      sprite.move()
    })
  };

  /**
     * 清除超出显示范围的精灵元素
     */
  clearSprite () {
    this.sprites.forEach((sprite, index) => {
      if (sprite.x > this.width || this.sprites[index].y > this.height ||
                sprite.x + sprite.width < 0 ||
                sprite.y + sprite.height < 0) {
        delete this.sprites[index] // 删除相应对象
        this.sprites = this.sprites.slice(0, index).concat(this.sprites.slice(index + 1, this.sprites.length)) // 清除数组中该位置
      }
    })
  };
}
/**
 * 计算FPS
 */
Frame.prototype.countFps = (function () {
  let lastLoop = (new Date()).getMilliseconds()
  let count = 1
  let fps = 0

  return function () {
    let currentLoop = (new Date()).getMilliseconds()
    if (lastLoop > currentLoop) {
      fps = count
      count = 1
    } else {
      count += 1
    }
    lastLoop = currentLoop
    return fps
  }
})()
module.exports = Frame


================================================
FILE: app/lib/danmu/index.js
================================================
'use strict'

const coordinator = require('electron').remote.getGlobal('coordinator')
const Player = require('./player')
const player = new Player()

// 监听得到弹幕的事件
coordinator.on('gotDanmu', data => {
  player.parseDanmus(data)
  player.controlDanmu('update')
})
// 监听操作相关事件
coordinator.on('danmuControl', data => {
  window.danmuControl[data].call()
})
// 删除弹幕事件
coordinator.on('deleteDanmu', data => {
  player.deleteDanmus(data.ids)
})

function send (param) {
  player.parseDanmus([param])
  player.controlDanmu('update')
};

function init (config, listener, object) {
  if (config.image.preload) {
    config.image.whitelist.map(url => {
      let img = window.document.createElement('img')
      img.width = 0
      img.height = 0
      img.src = url
      window.document.body.appendChild(img)
    })
  }
  player.setup(object, 'canvas-danmu')
  player.controlDanmu('play')
  window.setInterval(function () {
    coordinator.emit('fps', player.frame.fps)
  }, 1000)
  window.console.log('弹幕初始化完成!')
};

module.exports = {
  init: init,
  send: send,
  example: function () {
    let i = 1
    let id = 10000
    setInterval(function () {
      send({
        text: '[IMG WIDTH=24]danmu-24.png[/IMG]测试[IMG WIDTH=24]danmu-24.png[/IMG]Hello World[IMG WIDTH=24]danmu-24.png[/IMG]',
        color: 'rgb(' + parseInt(Math.random() * 255) + ',' + parseInt(Math.random() * 255) + ',' + parseInt(Math.random() * 255) + ')',
        lifeTime: 500,
        textStyle: 'normal bold ' + i + 'em 微软雅黑',
        height: i * 10,
        id: id
      })
      i++
      if (i > 5) i = 1
    }, 10)
  },
  stop: function () {
    player.frame.stopDanmu()
  },
  start: function () {
    player.frame.restartDanmu()
  },
  clear: function () {
    player.frame.clearDanmu()
  }
}

// 给window添加私货方便调试
window.danmuControl = module.exports


================================================
FILE: app/lib/danmu/player.js
================================================
'use strict'

let path = require('path')
let fs = require('fs')
let url = require('url')
let CommentFrame = require('./commentframe')
/**
 * 已经审核过的安全Url缓存
 * @type {Array}
 */
let safeUrl = []
/**
 * 检测URL是否合法
 * @param  string content
 * @return bool        
 */
function checkUrlValidate (content) {
  let regex = config.image.regex
  regex.lastIndex = 0
  let ret = null
  while ((ret = regex.exec(content)) !== null) {
    let unbelieveUrl = ret[2]
    if (safeUrl[unbelieveUrl]) continue // 加载缓存
    let parsedUrl = url.parse(unbelieveUrl)
    if (parsedUrl.protocol) { // 如果是网络协议就检查白名单
      return (config.image.whitelist.indexOf(unbelieveUrl) >= 0)
    }
    let safePath = path.join('/', unbelieveUrl)
    let filePath = path.resolve('./' + safePath)
    let unsafePath = path.resolve(unbelieveUrl)
    if (filePath !== unsafePath) {
      return false // 文件在上级目录或其他目录,判定为非法
    }
    if (!fs.existsSync(unsafePath)) { // 文件不存在,判定为非法
      return false
    }
  }
  return true
}
/**
 * 弹幕播放器
 * @constructor
 * @param {Integer} insertElement
 * @param {DOMElement} danmuConfig
 */
class Player {
  constructor (insertElement, danmuConfig) {
    this.insertElement = null
    // 绘制canvas相关的组件
    this.canvas = null
    this.frame = null
    // 存放解析好的弹幕内容
    this.danmus = []
    this.config = danmuConfig
  }

  /**
     * 初始化方法
     */
  setup (insertObject, elementId) {
    this.insertElement = insertObject
    let w = this.insertElement.offsetWidth // 控件的宽
    let h = this.insertElement.offsetHeight // 控件的高
    this.canvas = this.addCanvasElement(elementId, w, h)
    this.insertElement.parentNode.insertBefore(this.canvas, this.insertElement)
    let canvasContext = this.canvas.getContext('2d')
    this.frame = new CommentFrame(w, h, canvasContext)
  }

  /**
     * 控制弹幕
     * @param action
     */
  controlDanmu (action) {
    if (action === 'play') {
      this.frame.begin()
    } else if (action === 'stop') {
      this.frame.stop()
      this.frame.clearDanmu()
      this.frame.render()
    } else if (action === 'update') {
      this.addDanmu()
    }
  }

  /**
     * 创建canvas元素
     */
  addCanvasElement (elementId, width, height) {
    let e = window.document.createElement('canvas')
    e.id = elementId
    e.style.position = 'absolute'
    e.style.zIndex = '1000000'
    e.style.display = 'block'
    e.width = width
    e.height = height
    return e
  }

  /**
     * 将从服务器取得所有弹幕的内容,进行解析,放入this.danmus
     */
  parseDanmus (jsonResp) {
    let nowTime = (new Date()).valueOf()

    this.danmus = []
    jsonResp.forEach(danmu => {
      // 先检测图片弹幕
      if (config.display.image) {
        if (!checkUrlValidate(danmu.text)) return
      }
      danmu.font = danmu.textStyle
      danmu.lifeTime4TimeStamp = danmu.lifeTime * 1000 / 60
      danmu.addTime = nowTime
      danmu.height = parseInt(danmu.height)
      danmu.lifeTime = parseInt(danmu.lifeTime)
      this.danmus.push(danmu)
    })
  }

  /**
     * 弹幕精灵添加
     */
  addDanmu () {
    this.danmus.forEach(info => {
      if (info.style === 'custom') {
        this.frame.addCustomSprite(info)
      } else {
        this.frame.addSprite(info)
      }
    })
  }

  /**
     * 弹幕精灵删除
     */
  deleteDanmus (ids) {
    this.frame.deleteSprites(ids)
  }

  /**
     * 显示/隐藏弹幕的处理函数
     */
  toggleDanmu () {
    if (this.frame.visible) { // 弹幕可见
      this.frame.clearDanmu() // 情况当前所有待渲染弹幕
      this.frame.render() // 重绘一帧空的屏幕
      this.frame.stop() // 停止Frame
      this.frame.visible = false // 设置弹幕标记为不可见
    } else { // 弹幕隐藏
      this.frame.begin()
      this.frame.visible = true
    }
  }
}
module.exports = Player


================================================
FILE: app/lib/danmu/sprite.js
================================================
'use strict'

/**
 * 精灵对象
 * 所有的动画元素都必须继承自此对象,继承之后自动拥有move方法、速度属性、删除自身方法.
 * 每个动画元素都必须拥有一个自己的特殊的draw()方法的实现,这个方法用来在渲染每一帧的时候指定自己如何呈现在每一帧(frame.js)上
 * @param id
 * @param x 精灵相对与画布的位置
 * @param y
 * @param width 精灵的宽
 * @param height
 * @param speed
 * @param lifeTime
 * @param alive
 * @returns {Sprite}
 */
class Sprite {
  constructor (id, x, y, width, height, speed, lifeTime, alive) {
    this.id = id || 0
    this.x = x || 0
    this.y = y || 0
    this.width = width || 0
    this.height = height || 0
    this.lifeTime = lifeTime || 0
    this.alive = alive || true
    this.children = []
    /**
         * 精灵移动速度
         */
    this.speed = speed || {
      x: 0,
      y: 0
    }
  }

  draw () {

  }

  move () {
    this.x += this.speed.x
    this.y += this.speed.y
    if (typeof this.children !== 'undefined') {
      for (let i = 0; i < this.children.length; i++) {
        this.children[i].speed = this.speed
        this.children[i].move()
      }
    }
  }
  /**
         * 向此精灵添加一个子精灵
         */
  appendChild (sprite) {
    this.children.push(sprite)
  }
  /**
         * 渲染子精灵
         */
  drawChildren () {
    this.children.forEach(function (child) {
      child.draw()
    })
  }

  /**
     * 删除自身
     */
  remove () {
    this.lifeTime = 0
    this.alive = false
  };
}

module.exports = Sprite


================================================
FILE: app/lib/listener/index.js
================================================
/* global config */
'use strict'
const packageJson = require('../../package.json')
const player = require('../danmu/index')
const Socket = require('socket.io-client')
const coordinator = require('electron').remote.getGlobal('coordinator')

let io
let serverRandomNumber = null
module.exports = {
  init: function () {
    io = Socket(config.socket.url)
    io.heartbeatTimeout = config.socket.heartbeat
    realInit(config)
  }
}

function realInit () {
  let initCount = 0
  io.on('init', () => {
    initCount++
    io.emit('password', {
      password: config.socket.password,
      room: config.socket.room,
      info: {
        version: packageJson.version
      }
    })
    if (initCount > 1) {
      window.console.log('连接密码错误')
    }
  })
  io.on('connected', data => {
    initCount = 0
    window.console.log('已连接上弹幕服务器(' + data.version + ')')
    if (serverRandomNumber !== data.randomNumber) {
      if (serverRandomNumber !== null) {
        window.console.log('服务器似乎已重启,将清空弹幕池。')
        // 如果断线(服务器重启?)了,必须清理原有弹幕,否则会导致ID池不匹配
      }
      player.stop()
      player.clear()
      player.start()
      serverRandomNumber = data.randomNumber
    }
  })
  io.on('disconnect', () => {
    window.console.warn('与服务器的连接中断')
  })
  io.on('danmu', data => {
    window.console.log('得到' + data.data.length + '条弹幕')
    coordinator.emit('gotDanmu', data.data)
  })
  io.on('delete', data => {
    window.console.log('删除' + data.ids.length + '条弹幕')
    coordinator.emit('deleteDanmu', data)
  })
};


================================================
FILE: app/lib/utils/index.js
================================================
'use strict'
const url = require('url')
const path = require('path')

class ImageCache {
  constructor () {
    this.cache = []
    this.regex = null
  }

  test (callback, text) {
    if (!this.regex) return
    // Initialize here
    let ret = null
    this.regex.lastIndex = 0
    // Analyze text
    while ((ret = this.regex.exec(text)) !== null) {
      let src = ret[2]
      let imageWidth = parseInt(ret[1])
      let imageObject = this.getImage(src)
      if (imageObject === null) {
        imageObject = this.buildCache(src, imageWidth)
      }
      callback(ret, imageObject)
    }
    this.regex.lastIndex = 0
  }

  getImage (src) {
    return this.cache[src] || null
  }

  buildCache (src, width) {
    let parsedUrl = url.parse(src)
    let image = window.document.createElement('img')
    image.src = parsedUrl.protocol ? src : path.resolve('./', './' + src)
    image.width = width
    image.onerror = function () {
      window.console.error('Cannot load ' + src)
      this.cache[src].error = true
    }
    this.cache[src] = {
      error: false,
      element: image,
      width: width
    }
    return this.cache[src]
  }
}
let images = new ImageCache()

function tryCatch (fn, fnCatch) {
  try {
    fn()
  } catch (e) {
    fnCatch(e)
  }
}

module.exports = {
  imageAnalyzer: images,
  tryCatch
}


================================================
FILE: app/main.js
================================================
const electron = require('electron')
const path = require('path')
const {app, BrowserWindow} = electron
global.coordinator = new (require('events').EventEmitter)()
global.windows = {}

coordinator.on('exit', () => {
  app.exit(0)
})
app.on('window-all-closed', () => {
  app.exit(0)
})

app.on('ready', () => {
  windows.panelWindow = new BrowserWindow({
    width: 390,
    height: 150,
    resizable: false,
    icon: `${__dirname}/danmu.png`
  })
  windows.panelWindow.loadURL(`file://${__dirname}/panel.html`)
  windows.panelWindow.on('closed', () => {
    // app.exit();
  })
  windows.panelWindow.setMenu(null)

  windows.mainWindow = new BrowserWindow({
    transparent: true,
    frame: false,
    toolbar: false,
    resizable: true,
    title: 'DANMU Client',
    alwaysOnTop: true,
    icon: path.join(__dirname, '/danmu.png')
  })
  windows.mainWindow.loadURL(`file://${__dirname}/index.html`)
  windows.mainWindow.on('closed', () => {
    coordinator.emit('exit')
  })
  windows.mainWindow.setMenu(null)
})


================================================
FILE: app/package.json
================================================
{
  "name": "danmu-client",
  "version": "1.1.0-dev",
  "description": "Danmu client",
  "main": "./main.js",
  "author": "zsx <zsx@zsxsoft.com>",
  "license": "MIT",
  "homepage": "https://github.com/zsxsoft/danmu-client",
  "dependencies": {
    "socket.io-client": "^2.0.3",
    "windows-caption-color": "0.0.6"
  }
}


================================================
FILE: app/panel.html
================================================
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <style type="text/css">
    body {
        font-family: 'Segoe UI Light', 'Segoe UI', 'Segoe WP', 'Helvetica Neue', '微软雅黑 Light', '微软雅黑', sans-serif;
        font-size: 1rem;
        line-height: 1.5;
        color: #373a3c;
    }
    
    .btn {
        display: block;
        display: block;
        cursor: pointer;
        -webkit-transition: -webkit-transform .2s;
        float: left;
        min-width: 75px;
        text-align: center;
        opacity: .75;
        background-color: #2e8bcc;
        z-index: 1;
        margin-right: 2px;
        color: #fff;
        height: 70px;
        width: 70px;
        font-family: 'Segoe UI Light', 'Segoe UI', 'Segoe WP', 'Helvetica Neue', '微软雅黑 Light', '微软雅黑', sans-serif;
        border: none;
    }
    
    .btn:hover {
        opacity: 1;
    }
    
    .center {
        position: absolute;
        top: 10%;
        transform: translate(10%, 10%);
    }
    </style>
    <title>DANMU Panel</title>
</head>

<body>
    <div class="center">
        <button class="btn btn-success btn-control" data-top="danmuControl" data-param="start">接收弹幕</button>
        <button class="btn btn-success btn-control" data-top="danmuControl" data-param="stop">停收弹幕</button>
        <button class="btn btn-success btn-control" data-top="danmuControl" data-param="clear">清空弹幕</button>
        <button class="btn btn-success btn-control" data-top="windowControl" data-param="quit" id="btn-quit">退出弹幕</button>
    </div>
</body>
<script src="panel.js"></script>

</html>


================================================
FILE: app/panel.js
================================================
(function () {
  const electron = require('electron')
  const windows = electron.remote.getGlobal('windows')
  const coordinator = electron.remote.getGlobal('coordinator')
  const controlButtons = Array.from(window.document.querySelectorAll('.btn-control')) // I think querySelectorAll's api is terrible.
  let countQuitValue = 0
  let isShow = true

  function controlButtonClick () {
    coordinator.emit(this.getAttribute('data-top'), this.getAttribute('data-param'))
  }

  coordinator.on('fps', fps => {
    if (!isShow) return
    document.title = 'FPS: ' + fps
  })

  window.addEventListener('beforeunload', e => {
    // Hide but not exit
    // We cannot call a function that in a unregistered window.
    e.returnValue = 'false'
    windows.panelWindow.hide()
    isShow = false
  })

  window.addEventListener('keydown', e => {
    if (e.keyCode === 123) { // F12
      windows.panelWindow.webContents.openDevTools({
        detach: true
      })
    }
  }, true)

  document.querySelector('#btn-quit').addEventListener('click', () => {
    if (countQuitValue === 1) {
      coordinator.emit('exit')
    } else {
      setTimeout(() => {
        document.querySelector('#btn-quit').innerText = '退出程序'
        countQuitValue = 0
      }, 5000)
      this.innerText = '再按一次'
      countQuitValue = 1
    }
    return false
  })

  controlButtons.forEach(item => {
    item.addEventListener('click', controlButtonClick)
  })

  require('windows-caption-color').get((err, ret) => {
    if (!err) {
      window.document.body.style.background = 'rgba(' + ret.reg.r + ', ' + ret.reg.g + ', ' + ret.reg.b + ', ' + ret.reg.a + ')'
    }
  })
})()


================================================
FILE: appveyor.yml
================================================
environment:
  nodejs_version: "8"

cache:
  - node_modules
  - '%APPDATA%\npm-cache'
  - '%USERPROFILE%\.electron'

install:
  - ps: Install-Product node $env:nodejs_version
  - npm install

test_script:
  - node --version
  - npm --version

build_script:
  - npm run release

shallow_clone: true
clone_depth: 1

================================================
FILE: config.js
================================================
({
	socket: {
		url: "http://127.0.0.1:3000",
		password: "",
		room: "unlimited",
		heartbeat: 3000
	},
	display: {
		comment: {
			animationStyle: "scroll",
			fontStyle: "normal bold 5em 微软雅黑",
			fontColor: "rgb(255, 255, 255)",
			lifeTime: 240,
			height: 50
		},
		image: true
	},
	image: {
		regex: /\[IMG WIDTH=(\d+)\](.+?)\[\/IMG\]/ig,
		whitelist: [
			"https://www.baidu.com/img/bd_logo1.png",
			"http://www.baidu.com/img/bd_logo1.png",
		]
	}
});


================================================
FILE: package.json
================================================
{
  "name": "danmu-client",
  "version": "1.1.0",
  "description": "Danmu client",
  "main": "app/main.js",
  "scripts": {
    "start": "electron .",
    "build": "electron-builder",
    "release": "build"
  },
  "build": {
    "productName": "danmu-client",
    "appId": "com.zsxsoft.danmu.client",
    "compression": "maximum",
    "extraFiles": [
      "config.js"
    ],
    "mac": {
      "category": "public.app-category.utilities",
      "target": [
        "zip"
      ]
    },
    "win": {
      "target": [
        "7z"
      ]
    },
    "linux": {
      "target": [
        "zip"
      ]
    },
    "directories": {
      "buildResources": "resources"
    }
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/zsxsoft/danmu-client.git"
  },
  "keywords": [
    "danmu",
    "danmaku"
  ],
  "author": "zsx <zsx@zsxsoft.com>",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/zsxsoft/danmu-client/issues"
  },
  "homepage": "https://github.com/zsxsoft/danmu-client",
  "dependencies": {
    "socket.io-client": "^2.0.3",
    "windows-caption-color": "0.0.6"
  },
  "devDependencies": {
    "7zip-bin": "^2.2.3",
    "babel-eslint": "^7.2.3",
    "electron": "^1.7.5",
    "electron-builder": "^19.22.1",
    "eslint": "^4.5.0",
    "eslint-config-standard": "^10.2.1",
    "eslint-plugin-import": "^2.7.0",
    "eslint-plugin-node": "^5.1.1",
    "eslint-plugin-promise": "^3.5.0",
    "eslint-plugin-standard": "^3.0.1",
    "node-gyp": "^3.6.2"
  }
}


================================================
FILE: readme.md
================================================
danmu-client
==========
[![Github All Releases](https://img.shields.io/github/downloads/zsxsoft/danmu-client/total.svg)](https://github.com/zsxsoft/danmu-client/releases)
[![David deps](https://david-dm.org/zsxsoft/danmu-client.svg)](https://david-dm.org/zsxsoft/danmu-client)

这是一个独立的弹幕客户端,其服务端项目见[danmu-server](https://github.com/zsxsoft/danmu-server)。屏幕截图见[Release](https://github.com/zsxsoft/danmu-client/releases)。

**欲使用此项目,服务端需要使用对应的版本。[已发布的客户端](https://github.com/zsxsoft/danmu-client/releases)均已写明对应的服务端版本号,开发分支内的客户端版本仅对应开发分支的服务端。**

## 功能特色
- 以``WebSocket``作为通讯协议,用``Canvas``作为弹幕的画布;
- 可在桌面任何位置显示弹幕,可与其他程序(如PowerPoint、视频播放器等)共同工作;
- 窗口置于最前,完全透明可穿透,能与其他程序正常交互;
- 提供紧急清空弹幕池、停止接收弹幕等功能,可删除单条弹幕;
- 支持图片弹幕。

### 适用场景
- 数十人至千人集会(如学校活动、电影放映会)的实时互动
- 大型活动实时公告信息显示
- 欲在桌面显示实时吐槽

## 最低系统要求

Windows 7 / macOS 10.9 (x64) / Ubuntu 12.04 / Fedora 21 / Debian 8

## 使用预编译版本

1. 打开[Release](https://github.com/zsxsoft/danmu-client/releases)下载已经编译好的程序包并解压到某目录。
2. 双击目录下的``danmu-client``,启动成功。

## 从源码启动

1. 下载并安装[Nodejs](https://nodejs.org),并检查[node-gyp](https://github.com/nodejs/node-gyp)的依赖环境。
2. ``npm install``
3. ``npm start``

## 打包发布

``npm run build``

## 配置说明
根目录``config.js``下有配置,以下是说明

    socket: {
        url: "弹幕服务器开启的IP与端口(如使用反代,需要确认反代支持WebSocket连接)",
        password: "弹幕服务器连接密码",
        room: "选择连接弹幕服务器的某间房间",
        heartbeat: 心跳包发送间隔
    },
    display: {
        comment: {
            animationStyle: "默认弹幕样式(支持scroll、reversescroll、staticdown、staticup)",
            fontStyle: "默认字体样式",
            fontColor: "默认颜色",
            lifeTime: 每条弹幕的基本存活时间,
            height: 每条弹幕占据高度
        }, 
        image: 图片弹幕开关
    }, 
    image: {
        regex: 图片判断正则,勿动
        whitelist: [
            "图片弹幕允许加载的网络图片白名单。", 
            "不支持通配符,必须一条一条手动添加。", 
            "请确认图片可以正常被打开。"
        ], 
        preload: 网络图片是否预读缓存
    }

## 图片弹幕
打开图片弹幕开关后,弹幕内含相关内容的将被解析为图片。图片必须可以正常打开,调用代码如:``[IMG WIDTH=24]danmu-24.png[/IMG]``。格式:``[IMG WIDTH=图片宽度]图片地址(支持HTTP)[/IMG]``

为了保证安全与稳定,图片弹幕有防火墙机制。只有在弹幕程序目录及子目录下存在的图片才可被加载。引用网络图片,必须手动修改``config.js``添加白名单规则。如果被过滤,则程序不会有任何提示,该弹幕也不会被显示。

## 自定义弹幕
需要在服务器打开相应开关后,才允许使用自定义弹幕功能。自定义弹幕必须返回一个函数(或类),继承自``lib/danmu/sprite.js``中的``Sprite``,并需要实现``updateLifeTime``方法和``draw``方法,有``alive``属性。__为确保效率,自定义弹幕未加入错误捕捉。一旦函数出错,则弹幕系统停止接受新弹幕。__
示例代码如下(生成一个颜色随机、在屏幕上晃来晃去的玩意):

### 最新版示例代码 
```javascript
return (() => {
    'use strict';
    const Sprite = require('./lib/danmu/sprite');
    let canvasWidth = 0;
    let canvasHeight = 0;
    class Comment extends Sprite {
        constructor(param) {
            super(param.id, param.x, param.y, param.width, param.height, param.speed, param.lifeTime);
            this.text = param.text || ""; //文字内容
            this.lifeTime = param.lifeTime || config.display.comment.lifeTime;
            this.font = param.font || config.display.comment.fontStyle;
        }
        draw(canvasContext) {
            if (canvasWidth === 0) canvasWidth = canvasContext.canvas.width;
            if (canvasHeight === 0) canvasHeight = canvasContext.canvas.height;
            canvasContext.fillStyle = `rgb(${parseInt(Math.random() * 255)}, ${parseInt(Math.random() * 255)}, ${parseInt(Math.random() * 255)})`;
            canvasContext.font = this.font;
            canvasContext.fillText(this.text, parseInt(Math.random() * canvasWidth), parseInt(Math.random() * canvasHeight));
        }
        updateLifeTime() {
            this.lifeTime--; //每刷新一帧,存活时间-1
            this.alive = (this.lifeTime >= 0);
        };
    }
    return Comment;
})();

```


## 开源协议
The MIT License (MIT)


## 博文
[弹幕服务器及搭配之透明弹幕客户端研究结题报告](https://blog.zsxsoft.com/post/15)

[弹幕服务器及搭配之透明弹幕客户端研究中期报告](https://blog.zsxsoft.com/post/14)

[弹幕服务器及搭配之透明弹幕客户端研究开题报告](https://blog.zsxsoft.com/post/13)

## 开发者
zsx - https://www.zsxsoft.com / 博客 - https://blog.zsxsoft.com

## 感谢
[DDPlayer](https://github.com/dpy1123/ddplayer) by dpy1123
Download .txt
gitextract_7g7r0epy/

├── .eslintignore
├── .eslintrc
├── .gitattributes
├── .gitignore
├── .travis.yml
├── app/
│   ├── index.html
│   ├── index.js
│   ├── lib/
│   │   ├── danmu/
│   │   │   ├── comment.js
│   │   │   ├── commentframe.js
│   │   │   ├── frame.js
│   │   │   ├── index.js
│   │   │   ├── player.js
│   │   │   └── sprite.js
│   │   ├── listener/
│   │   │   └── index.js
│   │   └── utils/
│   │       └── index.js
│   ├── main.js
│   ├── package.json
│   ├── panel.html
│   └── panel.js
├── appveyor.yml
├── config.js
├── package.json
├── readme.md
└── resources/
    └── icon.icns
Download .txt
SYMBOL INDEX (56 symbols across 10 files)

FILE: app/index.js
  function initFunction (line 27) | function initFunction () {
  function keydownFunction (line 41) | function keydownFunction (e) {

FILE: app/lib/danmu/comment.js
  constant DW_TEXT (line 6) | const DW_TEXT = 1
  constant DW_IMAGE (line 7) | const DW_IMAGE = 2
  class Comment (line 12) | class Comment extends Sprite {
    method constructor (line 13) | constructor (param) {
    method draw (line 25) | draw (canvasContext) {
    method updateLifeTime (line 106) | updateLifeTime () {

FILE: app/lib/danmu/commentframe.js
  class CommentFrame (line 13) | class CommentFrame extends Frame {
    method constructor (line 14) | constructor (width, height, canvasContext) {
    method resize (line 46) | resize (width, height) {
    method addSprite (line 60) | addSprite (info) {
    method addCustomSprite (line 226) | addCustomSprite (info) {
    method render (line 252) | render () {
    method clearDanmu (line 269) | clearDanmu () {
    method stopDanmu (line 281) | stopDanmu () {
    method restartDanmu (line 289) | restartDanmu () {
    method updateSprite (line 296) | updateSprite () {
    method clearSprite (line 310) | clearSprite () {
    method deleteSprites (line 330) | deleteSprites (indexs) {

FILE: app/lib/danmu/frame.js
  class Frame (line 8) | class Frame {
    method constructor (line 9) | constructor (width, height, canvasContext) {
    method begin (line 33) | begin () {
    method render (line 51) | render () {
    method stop (line 61) | stop () {
    method addSprite (line 73) | addSprite (sprite) {
    method deleteSprites (line 80) | deleteSprites (indexs) {
    method updateSprite (line 93) | updateSprite () {
    method clearSprite (line 102) | clearSprite () {

FILE: app/lib/danmu/index.js
  function send (line 21) | function send (param) {
  function init (line 26) | function init (config, listener, object) {

FILE: app/lib/danmu/player.js
  function checkUrlValidate (line 17) | function checkUrlValidate (content) {
  class Player (line 46) | class Player {
    method constructor (line 47) | constructor (insertElement, danmuConfig) {
    method setup (line 60) | setup (insertObject, elementId) {
    method controlDanmu (line 74) | controlDanmu (action) {
    method addCanvasElement (line 89) | addCanvasElement (elementId, width, height) {
    method parseDanmus (line 103) | parseDanmus (jsonResp) {
    method addDanmu (line 124) | addDanmu () {
    method deleteDanmus (line 137) | deleteDanmus (ids) {
    method toggleDanmu (line 144) | toggleDanmu () {

FILE: app/lib/danmu/sprite.js
  class Sprite (line 17) | class Sprite {
    method constructor (line 18) | constructor (id, x, y, width, height, speed, lifeTime, alive) {
    method draw (line 36) | draw () {
    method move (line 40) | move () {
    method appendChild (line 53) | appendChild (sprite) {
    method drawChildren (line 59) | drawChildren () {
    method remove (line 68) | remove () {

FILE: app/lib/listener/index.js
  function realInit (line 18) | function realInit () {

FILE: app/lib/utils/index.js
  class ImageCache (line 5) | class ImageCache {
    method constructor (line 6) | constructor () {
    method test (line 11) | test (callback, text) {
    method getImage (line 29) | getImage (src) {
    method buildCache (line 33) | buildCache (src, width) {
  function tryCatch (line 52) | function tryCatch (fn, fnCatch) {

FILE: app/panel.js
  function controlButtonClick (line 9) | function controlButtonClick () {
Condensed preview — 24 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (49K chars).
[
  {
    "path": ".eslintignore",
    "chars": 21,
    "preview": "/http/res/*\nconfig.js"
  },
  {
    "path": ".eslintrc",
    "chars": 277,
    "preview": "{\n  \"root\": true,\n  \"parser\": \"babel-eslint\",\n  \"parserOptions\": {\n    \"sourceType\": \"module\"\n  },\n  \"env\": {\n    \"node\""
  },
  {
    "path": ".gitattributes",
    "chars": 378,
    "preview": "# Auto detect text files and perform LF normalization\n* text=auto\n\n# Custom for Visual Studio\n*.cs     diff=csharp\n\n# St"
  },
  {
    "path": ".gitignore",
    "chars": 1238,
    "preview": "/cache\n/builds\n/typings\n/out\n/dist\n\n# Logs\nlogs\n*.log\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented li"
  },
  {
    "path": ".travis.yml",
    "chars": 395,
    "preview": "osx_image: xcode8.3\nsudo: required\ndist: trusty\nlanguage: node_js\nnode_js: '8'\nenv:\n  global:\n  - ELECTRON_CACHE=$HOME/."
  },
  {
    "path": "app/index.html",
    "chars": 1235,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>DANMU Client</title>\n    <style type=\"text"
  },
  {
    "path": "app/index.js",
    "chars": 2065,
    "preview": "'use strict';\n(() => {\n  const electron = require('electron')\n  const windows = electron.remote.getGlobal('windows')\n  /"
  },
  {
    "path": "app/lib/danmu/comment.js",
    "chars": 3353,
    "preview": "/* global config */\n'use strict'\n\nlet utils = require('../utils')\nlet Sprite = require('./sprite')\nconst DW_TEXT = 1\ncon"
  },
  {
    "path": "app/lib/danmu/commentframe.js",
    "chars": 8746,
    "preview": "/* global config */\n'use strict'\nlet Frame = require('./frame')\nlet Comment = require('./comment')\n\n/**\n * 弹幕frame对象,继承字"
  },
  {
    "path": "app/lib/danmu/frame.js",
    "chars": 2613,
    "preview": "'use strict'\n\n/**\n * 帧对象\n * 原理就是每到一定时间就清除canvas,然后调用当前帧里的所有的元素的draw()方法。将所有动画元素按照新的配置重画,从而生成动画。\n * 之后程序无需关心元素的重画,只需要调整元素"
  },
  {
    "path": "app/lib/danmu/index.js",
    "chars": 1824,
    "preview": "'use strict'\n\nconst coordinator = require('electron').remote.getGlobal('coordinator')\nconst Player = require('./player')"
  },
  {
    "path": "app/lib/danmu/player.js",
    "chars": 3627,
    "preview": "'use strict'\n\nlet path = require('path')\nlet fs = require('fs')\nlet url = require('url')\nlet CommentFrame = require('./c"
  },
  {
    "path": "app/lib/danmu/sprite.js",
    "chars": 1328,
    "preview": "'use strict'\n\n/**\n * 精灵对象\n * 所有的动画元素都必须继承自此对象,继承之后自动拥有move方法、速度属性、删除自身方法.\n * 每个动画元素都必须拥有一个自己的特殊的draw()方法的实现,这个方法用来在渲染每一帧"
  },
  {
    "path": "app/lib/listener/index.js",
    "chars": 1505,
    "preview": "/* global config */\n'use strict'\nconst packageJson = require('../../package.json')\nconst player = require('../danmu/inde"
  },
  {
    "path": "app/lib/utils/index.js",
    "chars": 1327,
    "preview": "'use strict'\nconst url = require('url')\nconst path = require('path')\n\nclass ImageCache {\n  constructor () {\n    this.cac"
  },
  {
    "path": "app/main.js",
    "chars": 1020,
    "preview": "const electron = require('electron')\nconst path = require('path')\nconst {app, BrowserWindow} = electron\nglobal.coordinat"
  },
  {
    "path": "app/package.json",
    "chars": 321,
    "preview": "{\n  \"name\": \"danmu-client\",\n  \"version\": \"1.1.0-dev\",\n  \"description\": \"Danmu client\",\n  \"main\": \"./main.js\",\n  \"author\""
  },
  {
    "path": "app/panel.html",
    "chars": 1582,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"UTF-8\">\n    <style type=\"text/css\">\n    body {\n        font-"
  },
  {
    "path": "app/panel.js",
    "chars": 1651,
    "preview": "(function () {\n  const electron = require('electron')\n  const windows = electron.remote.getGlobal('windows')\n  const coo"
  },
  {
    "path": "appveyor.yml",
    "chars": 312,
    "preview": "environment:\n  nodejs_version: \"8\"\n\ncache:\n  - node_modules\n  - '%APPDATA%\\npm-cache'\n  - '%USERPROFILE%\\.electron'\n\nins"
  },
  {
    "path": "config.js",
    "chars": 461,
    "preview": "({\n\tsocket: {\n\t\turl: \"http://127.0.0.1:3000\",\n\t\tpassword: \"\",\n\t\troom: \"unlimited\",\n\t\theartbeat: 3000\n\t},\n\tdisplay: {\n\t\tc"
  },
  {
    "path": "package.json",
    "chars": 1499,
    "preview": "{\n  \"name\": \"danmu-client\",\n  \"version\": \"1.1.0\",\n  \"description\": \"Danmu client\",\n  \"main\": \"app/main.js\",\n  \"scripts\":"
  },
  {
    "path": "readme.md",
    "chars": 3881,
    "preview": "danmu-client\n==========\n[![Github All Releases](https://img.shields.io/github/downloads/zsxsoft/danmu-client/total.svg)]"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the zsxsoft/danmu-client GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 24 files (39.7 KB), approximately 13.0k tokens, and a symbol index with 56 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!