Public
Edited
Jan 26, 2023
9 forks
19 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
play;
playbar.x = 0;
app.ticker.remove(playLoop);
app.ticker.add(playLoop);
}
Insert cell
Insert cell
playLoop = delta => {
playbar.x += 2;

// get all the notes in the next two pixels
// (there may be more than one on different notes)
const playNotes = notes.filter(note => {
// check a few pixels in advance as most the samples are silent at the very start
// so we want them to start playing early
const x = note.getBounds().x - 20;
return playbar.x > x && playbar.x < x + 2;
});

playNotes.forEach(note => {
tones[note.name].play(note.y);
});
}
Insert cell
tones = {
let tonesObj = {};
Object.keys(spritePositions).map(async name => {

// Observable need to write out each FileAttachment link as a string.
const file = {
fish: await FileAttachment("fish.wav"),
coin: await FileAttachment("coin@1.wav"),
flower: await FileAttachment("flower.wav"),
egg: await FileAttachment("egg.wav"),
feather: await FileAttachment("feather.wav"),
star: await FileAttachment("star.wav"),
question: await FileAttachment("question.wav"),
mushroom: await FileAttachment("mushroom.wav"),
ghost: await FileAttachment("ghost.wav"),
}[name]

// adjust the pitch slightly so the middle position is closer to a B
// (they are in different octaves. they sounded strange otherwise)
const pitchAdjust = {
fish: -3,
coin: -1,
flower: -4,
egg: 4,
feather: 3,
star: 4,
question: 3,
mushroom: 4,
ghost: -4
}[name];

// link the tone and connect the pitchShifter
// the pitchShifter will be used in the play method below
const tone = new Tone.Player(await file.url());
const shift = new Tone.PitchShift().toDestination();
tone.connect(shift);
// when the note is played convert it's Y position
// and apply the pitchAdjust to get close to the note it should be
tonesObj[name] = { play: y => {
const pitch = range(boundry.top, boundry.bottom, 6, -6, y);
shift.pitch = pitch + pitchAdjust;
tone.start();
} };
})
return tonesObj;
}
Insert cell
Insert cell
duplicateSprite = name => {
const sprite = getSprite(name);

// give it an id so we can delete it
const id = notes.length ? notes[notes.length - 1].id + 1 : 1;
sprite.id = id;

// store in the notes array and keep the name so we can use it to play them
sprite.name = name;
notes.push(sprite);
sprite.dragging = true;
app.stage.addChild(sprite);

// if the icon is left at the very edge delete it
// otherwise play the sound
sprite.on('mouseup', e => {
if(sprite.alpha !== 1) {
app.stage.removeChild(sprite);
deleteNote(id);
} else {
sprite.dragging = false;
tones[name].play(sprite.y);
}
});

// this will run if the user tries to drag an icon out the play area
// if we first be transparent then when they move the cursor of it'll delete
sprite.on('mouseout', e => {
if(sprite.alpha !== 1) {
app.stage.removeChild(sprite);
deleteNote(id);
}
});
sprite.on('mousedown', e => {
sprite.dragging = true;
});
sprite.on('mousemove', e => {
if(sprite.dragging) {

// restrict the icons to stay inside the play area
// and appear semi-transparent when at the edge
const { xOut, yOut } = outOfBounds(e.data.global.x, e.data.global.y);

sprite.y = yOut || e.data.global.y;
sprite.x = xOut || e.data.global.x;

sprite.alpha = xOut || yOut ? 0.5 : 1;
}
});
}
Insert cell
deleteNote = id => {
const idx = notes.findIndex( note => note.id === id );
notes.splice( idx, 1 );
}
Insert cell
Insert cell
outOfBounds = (xPos, yPos) => {

let xOut, yOut;
if(boundry.top > yPos) {
yOut = boundry.top;
} else if(boundry.bottom < yPos) {
yOut = boundry.bottom;
}

if(boundry.left > xPos) {
xOut = boundry.left;
} else if(boundry.right < xPos) {
xOut = boundry.right;
}

return { xOut, yOut }
}
Insert cell
boundry = {
const minYPercent = 29;
const maxYPercent = 92;
const minXPercent = 15 / devicePixelRatio;
const maxXPercent = 98 / devicePixelRatio;

const widthSegment = width / 100;
const heightSegment = height / 100;
return {
left: minXPercent * widthSegment,
right: maxXPercent * widthSegment,
top: minYPercent * heightSegment,
bottom: maxYPercent * heightSegment
}
}
Insert cell
Insert cell
{
["feather", "star", "fish", "question", "flower", "mushroom", "ghost", "coin", "egg"].forEach( ( name, idx) => {
const sprite = getSprite(name);

const x = ( 41 * (idx+0.2)) / devicePixelRatio;
const y = (height / 50) / devicePixelRatio;
var box = new PIXI.Graphics();
box.beginFill(0xFFFFFF);
box.lineStyle(1, 0x000000);
box.drawRect(x, y, 35/devicePixelRatio, 35/devicePixelRatio);
sprite.x = x+17.5/devicePixelRatio;
sprite.y = y+17.5/devicePixelRatio;
box.addChild(sprite);
app.stage.addChild(box);
sprite.on('mousedown', () => {
duplicateSprite(name)
});
});
}
Insert cell
background = {
const file = await FileAttachment("background@2.png");
const texture = PIXI.Texture.from(await file.url());
const sprite = new PIXI.Sprite(texture);

const image = await file.image();
sprite.scale.set( (width / image.width) / devicePixelRatio );
return sprite;
}
Insert cell
getSprite = (name) => {
const {x, y, width, height} = spritePositions[name];
const frame = new PIXI.Rectangle(
x,
y,
width,
height
);
const origin = new PIXI.Rectangle(
0,
0,
width,
height
)
const newTexture = new PIXI.Texture(texture, frame, origin, false, false);
const sprite = new PIXI.Sprite(newTexture);

sprite.buttonMode = true;
sprite.defaultCursor = 'pointer';
sprite.interactive = true;
sprite.anchor.set(0.5)
sprite.scale.set(0.6/devicePixelRatio);

return sprite;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more