Skip to content

Commit

Permalink
Update dependencies to use zCanvas 6.0.1 (#82)
Browse files Browse the repository at this point in the history
* Initial implementation
* Do not use Worker in instrument editor (samples show incorrectly)
* Optimize waveform renderer calculations
  • Loading branch information
igorski authored Jan 3, 2024
1 parent c82f625 commit 9e2235d
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 75 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"vue-select": "^3.11.2",
"vuedraggable": "^2.24.3",
"wa-overdrive": "^0.0.3",
"zcanvas": "^5.1.8",
"zcanvas": "^6.0.1",
"zmidi": "^1.2.1"
},
"license": "MIT"
Expand Down
122 changes: 66 additions & 56 deletions src/components/instrument-editor/components/waveform-renderer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2016-2023 - https://www.igorski.nl
* Igor Zinken 2016-2024 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
Expand All @@ -20,24 +20,28 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { sprite } from "zcanvas";
import { Sprite } from "zcanvas";
import type { IRenderer, StrokeProps, Point } from "zcanvas";
import Config from "@/config";
import OscillatorTypes from "@/definitions/oscillator-types";
import { bufferToWaveForm } from "@/utils/waveform-util";

type IUpdateHandler = ( table: number[] ) => void;

class WaveformRenderer extends sprite
let id = 0;

class WaveformRenderer extends Sprite
{
private table: number[];
private cache: HTMLCanvasElement | undefined;
private cached = false;
private updateHandler: ( table: number[] ) => void;
private drawHandler: ( ctx: CanvasRenderingContext2D ) => boolean;
private drawHandler: ( ctx: IRenderer ) => boolean;
private interactionCache: { x: number, y: number };
private updateRequested: boolean;
private enabled: boolean;
private color: string;
private strokeStyle: string;
public strokeProps: StrokeProps = { color: "red", size: 5 };
private points: Point[];

constructor( width: number, height: number, updateHandler: IUpdateHandler, enabled: boolean, color: string ) {
super({ x: 0, y: 0, width, height });
Expand All @@ -51,6 +55,9 @@ class WaveformRenderer extends sprite
this.updateHandler = updateHandler;
this.interactionCache = { x: -1, y: -1 };
this.updateRequested = false;
this.points = [];

this._resourceId + `wfr_${++id}`;
}

/* public methods */
Expand All @@ -64,9 +71,9 @@ class WaveformRenderer extends sprite
* set a reference to the current WaveTable we're displaying/editing
*/
setTable( table: number[] ): void {
this.cache = undefined;
this.cached = false;
this.table = table;
this.canvas?.invalidate(); // force re-render
this.invalidate(); // force re-render
}

/**
Expand All @@ -76,9 +83,13 @@ class WaveformRenderer extends sprite
if ( buffer === undefined ) {
return;
}
const { width, height } = this.canvas.getElement();
this.cache = bufferToWaveForm( buffer, this.color, width, height, window.devicePixelRatio ?? 1 );
this.canvas?.invalidate(); // force re-render
const { width, height } = this.canvas!.getElement();

this.canvas!.loadResource( this._resourceId, bufferToWaveForm( buffer, this.color, width, height, window.devicePixelRatio ?? 1 ))
.then(() => {
this.cached = true;
this.invalidate();
});
}

/**
Expand Down Expand Up @@ -138,13 +149,13 @@ class WaveformRenderer extends sprite
setColor( color: string ): void {
this.color = color;
if ( this.enabled ) {
this.strokeStyle = color;
this.strokeProps.color = color;
}
}

setEnabled( enabled: boolean ): void {
this.enabled = enabled;
this.strokeStyle = enabled ? this.color : "#444";
this.strokeProps.color = enabled ? this.color : "#444";
}

/**
Expand All @@ -153,88 +164,81 @@ class WaveformRenderer extends sprite
* rendering (when false, base draw behaviour will be executed afterwards)
* This can be used for conditional rendering overrides.
*/
setExternalDraw( handler: ( ctx: CanvasRenderingContext2D ) => boolean ): void {
setExternalDraw( handler: ( renderer: IRenderer ) => boolean ): void {
this.drawHandler = handler;
}

syncStyles( ctx: CanvasRenderingContext2D ): void {
ctx.strokeStyle = this.strokeStyle;
ctx.lineWidth = 5;
}

/* zCanvas overrides */

draw( ctx: CanvasRenderingContext2D ): void {
if ( this.drawHandler?.( ctx )) {
override draw( renderer: IRenderer ): void {
if ( this.drawHandler?.( renderer )) {
return;
}

if ( this.cache ) {
const { width, height } = ctx.canvas;
ctx.imageSmoothingEnabled = false;
ctx.fillRect( 0, 0, width, height );
ctx.drawImage( this.cache, 0, 0, width, height );

const { width, height } = this._bounds;

if ( this.cached ) {
renderer.drawImage( this._resourceId, 0, 0, width, height );
return;
}

this.syncStyles( ctx );
ctx.beginPath();

const canvasWidth = this._bounds.width;

let h = this._bounds.height,
l = this.table.length,
y = this._bounds.top + h,
ratio = ( l / canvasWidth );
if ( this.points.length !== Math.ceil( width )) {
// pool the Points list to prevent unnecessary garbage collection on excessive allocation
this.points.length = Math.ceil( width );
for ( let i = 0, l = this.points.length; i < l; ++i ) {
this.points[ i ] = this.points[ i ] ?? { x: i, y: 0 };
}
}
const ratio = ( this.table.length / width );
const y = this._bounds.top + height;

for ( let i = 0; i < canvasWidth; ++i ) {
for ( let i = 0; i < width; ++i ) {
const tableIndex = Math.round( ratio * i );
const point = ( this.table[ tableIndex ] + 1 ) * 0.5; // convert from -1 to +1 bipolar range
ctx.lineTo( i, y - ( point * h ));
const value = ( this.table[ tableIndex ] + 1 ) * 0.5; // convert from -1 to +1 bipolar range

this.points[ i ].y = y - ( value * height );
}
ctx.stroke();
ctx.closePath();
renderer.drawPath( this.points, "transparent", this.strokeProps );
}

handleInteraction( aEventX: number, aEventY: number, aEvent: Event ): boolean {
override handleInteraction( eventX: number, eventY: number, event: Event ): boolean {
if ( !this._interactive ) {
return false;
}
if ( this.isDragging ) {
if ( aEvent.type === "touchend" ||
aEvent.type === "mouseup" ) {
if ( event.type === "touchend" ||
event.type === "mouseup" ) {
this.isDragging = false;
return true;
}

// translate pointer position to a table value

let tableIndex = Math.round(( aEventX / this._bounds.width ) * this.table.length );
let tableIndex = Math.round(( eventX / this._bounds.width ) * this.table.length );
tableIndex = Math.min( this.table.length - 1, tableIndex ); // do not exceed max length
let value = ( 1 - ( aEventY / this._bounds.height ) * 2 );
let value = ( 1 - ( eventY / this._bounds.height ) * 2 );
this.table[ tableIndex ] = value;

const cache = this.interactionCache;

// these have been observed to be floating point on Chrome for Android

aEventX = Math.round( aEventX );
aEventY = Math.round( aEventY );
eventX = Math.round( eventX );
eventY = Math.round( eventY );

// smooth the surrounding coordinates to avoid sudden spikes

if ( cache.x > -1 ) {
let xDelta = aEventX - cache.x,
yDelta = aEventY - cache.y,
let xDelta = eventX - cache.x,
yDelta = eventY - cache.y,
xScale = xDelta / Math.abs( xDelta ),
yScale = yDelta / Math.abs( xDelta ),
increment = 0,
w = this._bounds.width,
h = this._bounds.height,
l = this.table.length;

while ( cache.x !== aEventX ) {
while ( cache.x !== eventX ) {
tableIndex = Math.round(( cache.x / w ) * l );
tableIndex = Math.min( l - 1, tableIndex ); // do not exceed max length
value = ( 1 - ( Math.floor(( yScale * increment ) + cache.y ) / h ) * 2 );
Expand All @@ -243,10 +247,10 @@ class WaveformRenderer extends sprite
++increment;
}
}
cache.x = aEventX;
cache.y = aEventY;
cache.x = eventX;
cache.y = eventY;

aEvent.preventDefault();
event.preventDefault();

// don't hog the CPU by firing the callback instantly

Expand All @@ -258,13 +262,19 @@ class WaveformRenderer extends sprite
});
}
}
else if ( aEvent.type === "touchstart" ||
aEvent.type === "mousedown" )
else if ( event.type === "touchstart" ||
event.type === "mousedown" )
{
this.isDragging = true;
return true;
}
return false;
}

override dispose(): void {
super.dispose();
this.table = undefined;
this.points = undefined;
}
}
export default WaveformRenderer;
53 changes: 35 additions & 18 deletions src/components/waveform-display/waveform-display.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
</template>

<script lang="ts">
import { canvas } from "zcanvas";
import { Canvas, type IRenderer, type Point } from "zcanvas";
import { mapState, mapGetters, mapMutations } from "vuex";
import Config from "@/config";
import WaveformRenderer from "@/components/instrument-editor/components/waveform-renderer";
Expand Down Expand Up @@ -101,6 +101,10 @@ export default {
type: Boolean,
default: true,
},
optimizeRenderer: {
type: Boolean,
default: false,
},
},
computed: {
...mapState({
Expand Down Expand Up @@ -188,7 +192,13 @@ export default {
},
},
mounted(): void {
this.canvas = new canvas({ width: this.width, height: this.height, fps: 60 });
this.canvas = new Canvas({
width: this.width,
height: this.height,
autoSize: false,
optimize: this.optimizeRenderer ? "auto" : "none",
fps: 60
});
this.canvas.setBackgroundColor( "#000000" );
this.canvas.insertInPage( this.$refs.canvasContainer );
this.canvas.getElement().className = "waveform-canvas";
Expand Down Expand Up @@ -319,47 +329,54 @@ export default {
let fadeDelay = FADE_IN_DELAY;
let fadeSamples = FADE_IN_TIME;
const { wfRenderer } = this;
const { wfRenderer } = this;
const points: Point[] = new Array( bufferSize );
// pool the Points to prevent garbage collector hit
for ( let i = 0; i < points.length; ++i ) {
points[ i ] = { x: 0, y: 0 };
}
const lastPoint = points[ points.length - 1 ];
wfRenderer.setExternalDraw(( ctx: CanvasRenderingContext2D ) => {
wfRenderer.setExternalDraw(( renderer: IRenderer ) => {
// when drawing inside the waveform editor, always render the shape
if ( wfRenderer.isDragging ) {
ctx.globalAlpha = 1;
renderer.setAlpha( 1 );
fadeDelay = FADE_IN_DELAY;
fadeSamples = FADE_IN_TIME;
return false;
}
getAnalysers()[ this.instrumentIndex ].getByteTimeDomainData( sampleBuffer );
const hasSignal = sampleBuffer.some( value => value !== CEIL );
ctx.fillRect( 0, 0, width, height );
wfRenderer.syncStyles( ctx );
if ( hasSignal ) {
fadeSamples = 0;
fadeDelay = 0;
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.moveTo( 0, ( sampleBuffer[ 0 ] / CEIL ) * HALF_HEIGHT );
renderer.setAlpha( 1 );
points[ 0 ].y = ( sampleBuffer[ 0 ] / CEIL ) * HALF_HEIGHT;
for ( let x = 0, i = 1; i < bufferSize; ++i, x += sampleSize ) {
const v = sampleBuffer[ i ] / CEIL;
const y = v * HALF_HEIGHT;
ctx.lineTo( x, y );
const point = points[ i ];
point.x = x;
point.y = v * HALF_HEIGHT;
}
ctx.lineTo( width, HALF_HEIGHT );
ctx.stroke();
lastPoint.x = width;
lastPoint.y = HALF_HEIGHT;
renderer.drawPath( points, "transparent", wfRenderer.strokeProps );
return true;
} else {
if ( ++fadeDelay >= FADE_IN_DELAY ) {
if ( fadeSamples < FADE_IN_TIME ) {
++fadeSamples;
ctx.globalAlpha = easeIn( fadeSamples, FADE_IN_TIME );
renderer.setAlpha( easeIn( fadeSamples, FADE_IN_TIME ));
}
} else {
ctx.globalAlpha = 0;
renderer.setAlpha( 0 );
}
}
return !this.renderWaveformOnSilence;
Expand Down

0 comments on commit 9e2235d

Please sign in to comment.