Skip to content

Commit

Permalink
Merge branch 'v5'
Browse files Browse the repository at this point in the history
  • Loading branch information
bhj committed Jul 23, 2020
2 parents ed46792 + ec1383c commit e798cda
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 309 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## v5.0.0 (beta)

**Breaking changes/migrating from 4.x:**

v5 has a new, simple API. Instead of `play()`, `pause()` and `syncTime()`, you now control the [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) loop and `render()` a frame at the [currentTime](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio#attr-currentTime). This also enables full rewind/random seek support. See the README for more on using `render()`.

## v4.0.0 (June 23, 2020)

**Breaking changes/migrating from 3.x:**
Expand Down
131 changes: 55 additions & 76 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,49 @@
cdgraphics
==========
# cdgraphics

A [CD+Graphics (CD+G)](https://en.wikipedia.org/wiki/CD%2BG) implementation in JavaScript that draws to an HTML5 canvas. It's based on the [player by Luke Tucker](https://github.com/ltucker/html5_karaoke) with improvements from [Keith McKnight's fork](https://github.com/kmck/karaoke).

* Fast (60fps) rendering using [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame)
* Supports audio synchronization
* Callback support for CD+G title background changes
* Optional forced background keying (transparency) and shadow effects
* ES2015+
* Not designed for server-side rendering
* 60fps rendering with [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame)
* Audio synchronization with [currentTime](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio#attr-currentTime)
* Optional background keying (transparency) and shadow effects
* Supports callback for CD+G title background color changes
* Supports rewind and random seek
* ES2015+ with no dependencies

Installation
------------
## Installation
```
$ npm i cdgraphics
```

Usage
-----
## API

### `new CDGraphics(canvas, [options])`

- `canvas`: Your [`<canvas>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas) element. Required.
- `options`: Optional object with one or more of the following:
- `options`: Object with one or more [options](#options).

```js
import CDGraphics from 'cdgraphics'

const canvas = document.getElementById('my-canvas')
const cdg = new CDGraphics(canvas, { forceKey: true }) // force background transparency
```

#### Options

| Property | Type | Description | Default
| --- | --- | --- | --- |
| forceKey | Boolean | Force backgrounds to be transparent, even if the CD+G title did not explicitly specify it. | `false`
| onBackgroundChange | Function | Called when the CD+G title's background color or alpha changes. The RGBA color is passed as an array like `[r, g, b, a]` with alpha being `0` or `1`. The reported alpha includes the effect of the forceKey option, if enabled. | `undefined` |
| shadowBlur | Number | [CanvasRenderingContext2D.shadowBlur](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowBlur) (You likely also want to enable forceKey) | `0` |
| shadowColor | String | [CanvasRenderingContext2D.shadowColor](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowColor) (Alpha is defaulted to `1` here so that any shadowBlur is visible without explicitly specifying this option) | `rgba(0,0,0,1)` |
| shadowOffsetX | Number | [CanvasRenderingContext2D.shadowOffsetX](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowOffsetX) | `0` |
| shadowOffsetY | Number | [CanvasRenderingContext2D.shadowOffsetY](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowOffsetY) | `0` |
| forceKey | boolean | Force backgrounds to be transparent, even if the CD+G title did not explicitly specify it. | `false`
| onBackgroundChange | function | Called when the CD+G title's background color or alpha changes. The RGBA color is passed as an array like `[r, g, b, a]` with alpha being `0` or `1`. The reported alpha includes the effect of the forceKey option, if enabled. | `undefined` |
| shadowBlur | number | [CanvasRenderingContext2D.shadowBlur](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowBlur) (You likely also want to enable forceKey) | `0` |
| shadowColor | string | [CanvasRenderingContext2D.shadowColor](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowColor) (Alpha is defaulted to `1` here so that any shadowBlur is visible without explicitly specifying this option) | `rgba(0,0,0,1)` |
| shadowOffsetX | number | [CanvasRenderingContext2D.shadowOffsetX](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowOffsetX) | `0` |
| shadowOffsetY | number | [CanvasRenderingContext2D.shadowOffsetY](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowOffsetY) | `0` |

**Note:** When using shadow* options, graphics will render smaller and/or offset to fit the canvas as needed to avoid clipping the shadow.

Methods
-------

### `load(array)`
### `.load(array)`

Takes an array of bytes and parses the CD+G instructions synchronously. This must be done before calling `play`. Here's an example using fetch:
Loads an array of bytes and parses the CD+G instructions. This must be done before calling `render()`.

```js
fetch(cdgFileUrl)
Expand All @@ -49,75 +53,50 @@ fetch(cdgFileUrl)
})
```

### `play()`
### `.render([number])`

Starts or resumes playback. Has no effect if already playing.
Renders the frame at the given playback position (in seconds). Calling without a parameter will re-paint the last-rendered frame to the canvas.

### `pause()`

Pauses playback. Has no effect if already paused.

### `setOptions(object)`

Sets one or more [options](#usage) on-the-fly.
This method is designed to be used with [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) and the [currentTime](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio#attr-currentTime) property of an HTMLMediaElement (usually an `<audio>` element). The following excerpt shows a basic render loop:

```js
// whether playing or paused...
cdg.setOptions({ forceKey: true })
let frameId

// methods for render loop
const play = () => {
frameId = requestAnimationFrame(play)
cdg.render(audio.currentTime)
}
const pause = () => cancelAnimationFrame(frameId)

// link to <audio> element
audio.addEventListener('play', play)
audio.addEventListener('pause', pause)
audio.addEventListener('ended', pause)
audio.addEventListener('seeked', () => cdg.render(audio.currentTime))
```

### `syncTime(number)`
See [the demo code](https://github.com/bhj/cdgraphics/blob/master/demo/demo.js) for a complete example.

Sets the last known audio position in seconds (s). This can be used with the
[timeupdate](https://developer.mozilla.org/en-US/docs/Web/Events/timeupdate) event of an audio element to keep the graphics synchronized:
### `.setOptions(object)`

```js
// your <audio> element
audio.addEventListener('timeupdate', () => cdg.syncTime(audio.currentTime))
```
Sets one or more [options](#options) and re-renders.

Example
-------
This creates and passes a `<canvas>` to the constructor, then downloads and plays the .cdg file:
## Demo

```js
const CDGraphics = require('cdgraphics')

// or your existing <canvas> element
const canvas = document.createElement('canvas')
document.body.appendChild(canvas)
canvas.width = 600
canvas.height = 432

const cdg = new CDGraphics(canvas)

// download, parse and play
fetch('your_file.cdg')
.then(response => response.arrayBuffer())
.then(buffer => {
cdg.load(new Uint8Array(buffer))
cdg.play()
})

```

Running the Demo
----------------

To see how it all comes together:
To run the demo and see how it all comes together:

1. Clone the repo
2. Place your audio and .cdg file in the `demo` folder
3. Update lines 1 and 2 of `demo/demo.js` with those filenames
4. `npm i`
5. `npm run demo`
4. `$ npm i`
5. `$ npm run demo`
6. Browse to `http://localhost:8080` (the demo is served by webpack-dev-server)

Resources
---------
## Resources

* [Jim Bumgardner's CD+G Revealed](http://jbum.com/cdg_revealed.html) document/specification
License
-------
## License
[ISC](https://opensource.org/licenses/ISC)
22 changes: 15 additions & 7 deletions demo/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,29 @@ document.addEventListener('DOMContentLoaded', () => {
const audio = document.getElementById('audio')
const canvas = document.getElementById('canvas')
const CDGraphics = require('../index.js')
let frameId

const cdg = new CDGraphics(canvas, {
forceKey: true,
onBackgroundChange: color => {
console.log('onBackgroundChange', color)
}
})

// link to audio events
audio.addEventListener('play', () => cdg.play())
audio.addEventListener('pause', () => cdg.pause())
audio.addEventListener('timeupdate', () => cdg.syncTime(audio.currentTime))
// methods for render loop
const play = () => {
frameId = requestAnimationFrame(play)
cdg.render(audio.currentTime)
}
const pause = () => cancelAnimationFrame(frameId)

// options UI
const forceKeyCheckbox = document.getElementById('force_transparent')
// link to audio events (depending on your app, not all are strictly necessary)
audio.addEventListener('play', play)
audio.addEventListener('pause', pause)
audio.addEventListener('ended', pause)
audio.addEventListener('seeked', () => cdg.render(audio.currentTime))

// demo options UI
const forceKeyCheckbox = document.getElementById('forceKey')
const shadowBlurRange = document.getElementById('shadowBlur')
const shadowOffsetXRange = document.getElementById('shadowOffsetX')
const shadowOffsetYRange = document.getElementById('shadowOffsetY')
Expand Down
38 changes: 21 additions & 17 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,36 @@
<meta charset="utf-8">
<title>cdgraphics demo</title>
<style type="text/css">
html { font-family: system-ui; }
body { background-image: linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(-45deg, transparent 75%, #ccc 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px; }
app { padding: 1em; }
#audio { display: block; }
fieldset { background: #f5f5f5; padding: 1rem; max-width: 600px; }
</style>
</head>
<body>
<div id="app">
<canvas id="canvas" width="600" height="432"></canvas>
<audio id="audio" controls></audio>
<br />
<label style="background: white;">
<input type="checkbox" id="force_transparent" value="1" checked />
Force key
</label><br /><br />
<label style="background: white;">
<input type="range" id="shadowBlur" min="0" max="32" value="0" />
shadowBlur
</label><br />
<label style="background: white;">
<input type="range" id="shadowOffsetX" min="0" max="32" value="0" />
shadowOffsetX
</label><br />
<label style="background: white;">
<input type="range" id="shadowOffsetY" min="0" max="32" value="0" />
shadowOffsetY
</label>
<fieldset>
<legend><label>Options</label></legend>
<label>
<input type="checkbox" id="forceKey" value="1" />
ForceKey
</label><br /><br />
<label>
<input type="range" id="shadowBlur" min="0" max="32" value="0" />
shadowBlur
</label><br />
<label>
<input type="range" id="shadowOffsetX" min="0" max="32" value="0" />
shadowOffsetX
</label><br />
<label>
<input type="range" id="shadowOffsetY" min="0" max="32" value="0" />
shadowOffsetY
</label>
</fieldset>
</div>
<script src="bundle.js"></script>
</body>
Expand Down
Loading

0 comments on commit e798cda

Please sign in to comment.