v 0.4.2
cc teddavis.org 2025
p5.js library to render graphics on analog vector displays.
XYscope.js converts the coordinates of primative shapes (point, line, rect, ellipse, vertex, box, sphere, torus, text...) to audio waveforms (oscillators with custom wavetables) which are sent to an analog display in XY-mode, revealing their vector graphic forms. This library began as XYscope for Processing in 2017 and has now been ported to p5.js and built from the ground up with the aim of live-coding oscilloscopes from your web browser in P5LIVE! Vector graphics shine on a vector display and now we can view (and hear) our generative works like never before!
teddavis.org/xyscopejs
github.com/ffd8/xyscopejs
Download and include locally, or via CDN:
<script src="https://cdn.jsdelivr.net/gh/ffd8/xyscopejs@0.4.2/xyscope.js"></script>
For live-coding in P5LIVE, include it in the libs array:
let libs = ['https://cdn.jsdelivr.net/gh/ffd8/xyscopejs@0.4.2/xyscope.js']
Here's a basic template to start live-coding within P5LIVE + XYscope.js.
Essentially, add xy.
and draw between clearWaves()
and buildWaves()
!
let libs = ['https://cdn.jsdelivr.net/gh/ffd8/xyscopejs@0.4.2/xyscope.js', 'includes/libs/xyscope.js']
let xy // XYscope.js instance
function setup() {
createCanvas(windowWidth, windowHeight)
xy = new XYscope(this) // set instance
xy.canvas() // resize canvas/scope to full height, centered
// xy2 = new XYscope(this, xy.outXY) // optional dup for additive-synth
}
function draw() {
clear()
xy.drawXY() // draw virtual scope
xy.clearWaves() // clear shapes buffer (like clear/background)
xy.circle(width/2, height/2, 100) // add to shapes buffer
xy.buildWaves() // build waves from shapes buffer
}
function mousePressed() {
xy.resume() // user click required for sound in browser
}
- quick start: P5LIVE / p5.js Editor
- more coming sooooon... first getting this out there!
When connecting to an actual oscilloscope, you'll need a Digital to Analog Converter (DAC) to send your stereo audio signal to the XY-mode of a given display. Here's a few options based on price:
At the very least you can use your computer's headphone jack with an 1/8" to RCA
cable. However you'll soon want to get a DC-Coupled audio interface for a cleaner and more stable visual (not wobbling/centering constantly).
For workshops I like to use some variant of the 48Khz Delock 61645 or 96Khz Delock 63926 – you'll find the same chip under many different brands and casings. For modern laptops, there's also a very compact 96Khz Delock USB-C version.
Many of us in the community found the MOTU Ultralite Mk3 Hybrid (or newer versions) to be an ideal audio interface. Price varies from used to new. It offers, 10-channels of DC-Coupled 196Khz output, which is useful to drive multiple oscilloscopes or RGB lasers (X, Y, R, G, B).
XYscope.js can only use the browser/system selected audio interface – however multi-channel support will soon be added for customizing which channels are used for multiple diplays or RGB lasers.
Now we need a vector display to see our glowing output!
XXY virtual oscilloscope rendering by Neil Thapen is built into XYscope.js! Activate it by simply adding xy.drawXY()
to your code. See rendering references below for complete options and customization.
This is what we really want! They have a Cathode-ray Tube (CRT) that is the magic behind this obsession. You'll find them for ~$50 used on auction websites – be sure it has 2-channels (z-axis input is a bonus) and that they show images of a sharp working beam. You'll need a few RCA to BNC
adaptors to interface with it. Have fun playing with all the knobs to put it into XY Mode
so that the 2-channels drive the beam X/Y (Horizontal/Vertical).
Similar to an analog oscilloscope, but usually has a larger display and reduced controls for X-Y (+Z) input, leaving away many of the features on an oscilloscope we won't use. They're more rare, expensive, but great if you stumble upon one. Don't confuse these with a 'vector monitor' which is used for calibrating TV broadcast and won't draw X-Y coordinates.
A vector-graphics video game system of the 1980s, these amazing 9" displays can be very carefully modified (CAREFUL - at own risk) to override (on-demand) the videogame control of the monitor's XYZ inputs. It's ideal to use switching jacks so videogames still works when cables are unplugged. You'll also want to apply the SPOT KILLER MOD, but BE SURE to apply an appropriately high-voltage rated switch, so it can be toggled on and off.
XYscope.js vectrex specific support is pending... it works, just not yet aspect ratio aware.
Once you want something bigger than most screens, you'll want to move to an RGB Laser. They're BIG and BRIGHT, but also much slower and more dangerous! It's slower because it mechanically moves galvos/mirrors for the X-Y and dangerous, because, LASERS! Nevertheless, they can be controlled via the ILDA analog input, for which I've developed an easy to build dac_ilda adaptor. To control a laser, you'll need a DAC (sound card) with a minimum of 5-channels, for sending X, Y, R, G, B
signals.
ILDA RGB Laser specific support is pending... working on multi-channel support next.
Something very unique to this workflow is sending multiple audio signals to the oscilloscope, which interfer and modulate one another. As these waves combine, their amplitude and frequency determine ones influence on other waves. Key is having different frequencies and amplitudes to modulate off one another. The lower the frequency, the more it will push other waves around. The lower the amp, the less influence it has on the additive waveform. We can easily patch multiple instances of XYscope.js into the same audio output, thus creating endless surprises as the waves interact.
let libs = ['https://cdn.jsdelivr.net/gh/ffd8/xyscopejs@0.4.2/xyscope.js', 'includes/libs/xyscope.js']
let xy // XYscope.js instance
function setup() {
createCanvas(windowWidth, windowHeight)
xy = new XYscope(this) // set instance
xy.canvas() // resize canvas/scope to full height, centered
xy2 = new XYscope(this, xy.outXY) // patch xy2 onto xy's audio out
}
function draw() {
xy.drawXY() // draw virtual scope
xy.clearWaves() // clear shapes buffer
xy.freq(50) // 50 is default hz of XYscope instance
xy.circle(width/2, height/2, 100) // add to shapes buffer
xy.buildWaves() // build waves from shapes buffer
xy2.clearWaves() // clear 2nd buffer
xy2.amp(.5) // experiment with amp/loudness of additive signal
xy2.freq(25.1) // some relationship to 1st xy freq value
// xy2.freq(225.1) // experiment with much higher freqs too!
xy2.circle(width/2, height/2, 100) // add to 2nd buffer
xy2.buildWaves() // build waves of 2nd buffer
}
function mousePressed() {
xy.resume() // user click enables sound in browser
}
Additional tips:
- No need to stop at just 2, add as many as needed!
- Ratio between freqs is crucial, diff of +/- .1 animates things.
- Really low frequencies animate shapes over that path.
- Really high frequencies display shape made of 2nd shape.
- Play with position of 2nd shape, from center to corners.
XYscope.js is a class, so after an instance has been defined to a variable, we'll use that prefix in front of every function listed below (scoped), ie: xy.ellipse()
. This enables us to have multiple XYscope.js instances running parallel, which will reveal wild and crazy audio/visuals.
All examples below use xy
as the instance prefix.
- point()
- line()
- square()
- rect()
- ellipseDetail()
- circle()
- ellipse()
- complex shape
- lissajous()
- box()
- sphere()
- ellipsoid()
- torus()
xy = new XYscope(this) // 'this' passes current instance of p5.js
xy.canvas() // optionally resize p5.js + XXY to full height, centered
xy2 = new XYscope(this, xy.outXY) //optional 2nd instance for additive-synth
Clears the wavetable of previous shapes, place near top of draw()
.
xy.clearWaves() // clear previous wavetables
Builds X and Y wavetables from buffer of added shapes. Place after you've drawn all shapes.
xy.buildWaves() // send shapes to wavetables/audio
If you prefer, simply provide your own X and Y arrays of values between -1.0 to 1.0 for custom wavetables.
tempX = new Array(128) // blank array
tempY = new Array(128) // blank array
for(let i = 0; i < tempX.length; i++) {
tempX[i] = noise(frameCount * .001 + i * .02) * 2 - 1 // pack it
tempY[i] = noise(frameCount * .0013 + i * .022) * 2 - 1 // pack it
}
xy.setWaves(tempX, tempY) // set XY wavetables to custom x, y arrays
Interpolates (adds) points between each coordinate for smooth lines (on by default).
xy.smooth() // smooth lines, default is 1
xy.smooth(newGap) // set custom gap size between points
Only draw exact coordinates of shapes, thus a rect()
is shown as 4 points.
xy.noSmooth() // only draw coordinate points
Prevent coordinates outside of canvas from being drawn (creates a wall/box of lines if not activated). Useful if scaling drawing/model beyond the size of canvas.
xy.limitPath() // uses width, height of canvas as border to limit path
xy.limitPath(newLimit) // set inner border (px) amount
xy.limitPath(-1) // disables limitPath
Most primitives from p5.js have been ported, so you simply need to add xy.
in front of them! They can also be used without parameters, for quickly testing.
Draw a single point.
xy.point() // defaults to random width/height
xy.point(x, y)
xy.point(x, y, z)
Draw a line between two coordinates, in 2D or 3D space.
xy.line() // defaults to random width/height
xy.line(x1, y1, x2, y2)
xy.line(x1, y1, z1, x2, y2, z2)
Draw a rectangle with same width and height.
rectMode(CENTER) // default CORNER, use CENTER to draw center out
xy.square() // defaults to (0, 0, 100)
xy.square(x, y, w)
Draw a rectangle with custom width and height.
rectMode(CENTER) // default CORNER, use CENTER to draw center out
xy.rect() // defaults to (0, 0, 100)
xy.rect(x, y, w) // uses w for h
xy.rect(x, y, w, h)
Global value for number of facades used for circle()
and ellipse()
.
xy.ellipseDetail() // get current facets of ellipse
xy.ellipseDetail(newVal) // set new count of facets, default 50
Draw a circle with same width and height.
xy.circle() // defaults to (0, 0, 100)
xy.circle(x, y, w)
xy.circle(x, y, w, numPoints) // numPoints overrides ellipseDetail
Draw a circle with custom width and height.
xy.ellipse() // defaults to (0, 0, 100)
xy.ellipse(x, y, w) // uses w for h
xy.ellipse(x, y, w, h)
xy.ellipse(x, y, w, h, numPoints) // numPoints overrides ellipseDetail
Draw a triangle with custom coordinates.
xy.triangle() // defaults to 100px, positioned at 0,0
xy.triangle(x1, y1, x2, y2, x3, y3) // set 3-coordinates
Draw a complex form using multiple vertices.
xy.beginShape()
xy.vertex() // defaults to random width/height
xy.vertex(x, y)
xy.vertex(x, y, z) // 3D coordinate space
// ...
xy.endShape()
xy.endShape(CLOSE) // closes form
Draw a lissajous curve, which depends on a certain ratio between A and B.
xy.lissajous() // defaults to infinity symbol
xy.lissajous(x, y, radius, ratioA, ratioB, phase) // uses ellipseDetail
xy.lissajous(x, y, radius, ratioA, ratioB, phase, numPoints) override ellipseDetail
Draw a 3D cube, optionally set the width, height, depth.
xy.box() // defaults to (100)
xy.box(w) // uses w for h and d
xy.box(w, h) // uses w for d
xy.box(w, h, d)
Draw a sphere, optionally set detailX/Y (mesh vertices) and toggle the rendering of latitude and longitude lines as a object passed in any parameter from 2 onward ie. {lat:false, long:true}
.
xy.sphere()
xy.sphere(radius)
xy.sphere(radius, opts) // {lat:0, long:1}
xy.sphere(radius, detailX) // default 24
xy.sphere(radius, detailX, detailY) // default 24, 16
xy.sphere(radius, detailX, detailY, opts) // {lat:0, long:1}
Draw an ellipsoid with optional custom radius in X/Y/Z dimensions, and toggle the rendering of latitude and longitude lines as a object passed in any parameter from 4 onward ie. {lat:false, long:true}
.
xy.ellipsoid()
xy.ellipsoid(rx, ry, rz)
xy.ellipsoid(rx, ry, rz, opts) // {lat:0, long:1}
xy.ellipsoid(rx, ry, rz, detailX) // default 24
xy.ellipsoid(rx, ry, rz, detailX, detailY) // default 24, 16
xy.ellipsoid(rx, ry, rz, detailX, detailY, opts) // toggle {lat:0, long:1}
Draw a tube shape with optional custom radius, tubeRadius, detail X/Y and toggle the rendering of latitude and longitude lines as a object passed in any parameter from 3 onward ie. {lat:false, long:true}
.
xy.torus()
xy.torus(radius, tubeRadius)
xy.torus(radius, tubeRadius, opts) // {lat:0, long:1}
xy.torus(radius, tubeRadius, detailX) // default 24
xy.torus(radius, tubeRadius, detailX, detailY) // default 24, 16
xy.torus(radius, tubeRadius, detailX, detailY, opts) // {lat:0, long:1}
XYscope.js has built in text rendering for Hershey fonts.
hersey_futural
is embedded, you can load others from the hershey fonts set.
xy.loadFont("path_to_font")
xy.loadFont('https://cdn.jsdelivr.net/gh/kamalmostafa/hershey-fonts/hershey-fonts/cursive.jhf')
Draw text.
xy.text() // defaults to ("XYscope", 0, 0)
xy.text("string", x, y)
xy.text("hello\nworld", x, y) // use '\n' for multi-line text
xy.text(`hello
world`, x, y) // or use `` (literals) for multi-line text
Get coordinates of Hershey text for manipulating type!
Returns an 2D array of coordinates: chars[ coords[] ]
let textPath = xy.textPaths("XYscope", x, y)
for(let char of textPath) {
xy.beginShape()
for(let c of char) {
xy.vertex(c.x, c.y)
}
xy.endShape()
}
Get or set the text size.
xy.textSize() // get current textSize
xy.textSize(newSize) // set new textSize
Get or set the text leading.
xy.textLeading() // get current textLeading
xy.textLeading(newSize) // set new textLeading
Set the text alignment on horizontal and optionally vertical axis.
xy.textAlign(hAlign) // Horz: LEFT (default) / CENTER / RIGHT
xy.textAlign(hAlign, vAlign) // Vert options: TOP / CENTER / BOTTOM
Get the width in pixels of a text string for positioning or drawing around.
xy.textWidth("string") // get width (px) of text
Frequency of oscillators, used to adjust the speed of the beam, adjust it's musical or sonic qualities and very important for additive-synthesis.
// get
xy.freq() // returns object (.x, .y) of freqs
xy.freq().x // returns frequency of x oscillator
// set
xy.freq(freqXY) // set both X/Y levels, default is 50.0
xy.freq(freqX, freqY) // set x, y to separate frequencies
Amplitude of oscillators, used to adjust how loud a given oscillator is. Can be very quite, or amplified beyond normal range for distortion.
// get
xy.amp() // returns object (.x, .y) of amps
xy.amp().x // returns amplitude of x oscillator
// set
xy.amp(ampXY) // set both X/Y levels, default is 1.0
xy.amp(ampX, ampY) // set x, y to separate amplitudes
A very experimental high-pass filter has been implemented.
xy.highpass(freq) // set cutoff for high-pass filter
A very experimental low-pass filter has been implemented.
xy.lowpass(freq) // set cutoff for low-pass filter
There's a built-in step sequencer for XYscope.js! It allows you to algorithmicly code patterns that are then played back, setting the frequency of your XY oscillators to those notes aka let's make music! This sequencer will soon be released separately as a library, since it can be used to trigger anything – but was designed for XYscope performances.
The sequencer is already activated on each XYscope instance, and is given the variable scope of seq
, ie use xy.seq
when changing settings. Be sure to disable xy.freq()
, as the two would compete for setting frequency.
We can adjust a few settings for our sequences
xy.seq.bpm() // get bpm
xy.seq.bpm(120) // set bpm
xy.seq.octave() // get default octave for notes, default 3
xy.seq.octave(2) // set custom default octave, ie 1 - 7
xy.seq.duration() // set step/rest duration length, default 8 as in 1/8
xy.seq.duration(dur) // set step/rest duration length, ie 1 – 64
xy.seq.start() // start sequencer
// xy.seq.stop() // stop sequencer
xy.seq.loop = true // default on, set to false for single sequence
xy.mute = false // toggle to silence
There's a special notation for adjusting duration, octave, repeats, alternates that is inspired by Strudel:
xy.seq.pattern('c')
// plays C in default octave (3)xy.seq.pattern('C')
// plays C in default octave (3)xy.seq.pattern('c d e f g a b c4')
// C Major scale
xy.seq.pattern('a')
// repeated notexy.seq.pattern('a ')
// repeated note, restxy.seq.pattern('a ')
// repeated note, double restxy.seq.pattern('a--')
// repeated note, double rest (can use ' ' or '-')xy.seq.pattern('a 32r d 16r f 4r')
// use#r
for custom rest length
xy.seq.pattern('a c')
// sequence of notes, no restsxy.seq.pattern('a c ')
// sequence of notes, rest only at endxy.seq.pattern('a c ')
// sequence of notes, rest after eachxy.seq.pattern('a- c-')
// rest after each, more clear
xy.seq.pattern('a# cb')
// use#
for sharp,b
for flat
xy.seq.pattern('a2 ')
// set custom octave with number after note valuexy.seq.pattern('4a ')
// set custom duration with first value, here 1/4xy.seq.pattern('2a3 ')
// set custom octave and durationxy.seq.pattern('2a3*3 ')
// with*3
makes a triplet,*1
is default
xy.seq.pattern('a c:d:e:f')
// use:
for alternates to walk thruxy.seq.pattern('a c;d;e;f')
// use;
for alternates randomly selectedxy.seq.pattern('a:a2 d;8d4*4')
// 1st has:
walk, 2nd has;
randomxy.seq.pattern('A;C;D;E;G')
// random walk A minor pentatonic scalexy.seq.pattern('a:a2- f:f:e:e:d:d:d:d*3-')
// go wild!
A small collection of tunes and their patterns.
// close encounters of 3rd kind
xy.seq.pattern('4A3 4B3 4G3-- 4G2-- 2D3 2r 4A2 4B2 4G2-- 4G1-- 2D2 2r')
// more soon...
You can also trigger events whenever a new note is played or ended, ie draw a shape or change values on each note.
xy.seq.onStep((step) => {
// do something on each step
circle(random(width), random(height), 50)
})
xy.seq.onStepEnded((step) => {
// do something when step finished
})
pending...
pending...
A virtual scope is embedded via XXY by Neil Thapen! It offers a very impressive synthesis of glow and tracing artifacts similar to a real oscilloscope, but of course is match for the real thing. Nevertheless, there's plenty of options to adjust when rendering on screen.
xy.drawXY() // launch fullscreen with GUI
xy.drawXY({gui:0}) // hide GUI
// default options
var xyOptions = { // shorthand
toggle:1,
gui:1,
grid:0,
fullscreen: 1, // 'fs'
opacity:0.75, // 'o'
thickness:0.01, // 't'
hue:120, // 'h'
gain:0.1, // 'g'
intensity:-.0, // 'i'
persistence:-1 // 'p'
}
xy.drawXY(xyOptions) // set any number of options above
xy.drawXY({gui:0, fs:1, o:.75, t:.01, h:120, g:.1, i:.0, p:-1}) // shorthand
For further experiments, the XXY canvas is available as xy.scope
.
Beyond the virtual scope, there's plenty of interesting debug views to check out, from monitoring the waveform (wavetable) used for the oscillators, to the wave itself with time flowing through it.
xy.drawWaveform() // draw as oscillator waveform frozen
xy.drawWaveform(strokeWeight) // draw waveform frozen and set strokeWeight
xy.drawWave() // draw oscillator wave with time flowing
xy.drawWave(strokeWeight, color) // oscillator wave, set strokeWeight + color
xy.drawAll() // displays both above plus drawXY()
xy.drawShapes() // primatives also render in p5.js, not just as audio
xy.drawShapes(toggle) // default true (1), set to false (0) to hide
Since XYscope.js is intended for live-coding analog vector displays, it's important to easily view that real screen behind your code. For this, we can render an external webcam, which will re-scan your display. Normally with p5.js you'd need a small chunk of code to load and adjust the camera in your sketch, but this takes care of that all in the backend! While it's intended for capturing your oscilloscope or similar monitor, it's also just a fun and weird way to play with the camera!
This should be added within the draw()
, as it acts as both a setup and render of the camera, along with options to be changed on the fly. This can be instantly added as a short code, or see options for customizing further.
xy.cam() // BAM, you have a CAM!
// default options
var camOptions = { // shorthand
orientation: 'height', // 'o', fit to 'height' or 'width'
// scale: 1, // 's', set custom scale (overrides orientation)
horizontal: 1, // 'h', 0 - 1, for adjusting aspect ratio issues
vertical: 1, // 'v', 0 - 1, for adjusting aspect ratio issues
rotation:0, // 'r', rotate based on degrees
toggle:1, // 't', toggle cam display
}
xy.cam(camOptions)
xy.cam({o:1, h:1, v:1, r:0}) // shorthand
xy.cam({s:3, h:1, v:1, r:0}) // shorthand
For further experiments, the camera image is available as xy.capture
.
Found a bug, missing feature, and/or created a project with XYscope.js?
Let me know! Create an issue on GitHub.
This project is licensed under the GNU GPLv3 License - see LICENSE.md for details.
- Stefanie Bräuer, feeding the obsession with crucial theory + context.
- Just Van Rossum, the enlightening conversation on my X-Y attempts baaack in 2017.
- Neil Thapen, that amazing virtual oscilloscope rendering.