Skip to content

Commit be6f758

Browse files
committed
add external renderer layer for generated dancers
1 parent 37689e9 commit be6f758

File tree

5 files changed

+192
-15
lines changed

5 files changed

+192
-15
lines changed

src/ExternalDancer.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Thin p5 adapter: owns a p5.Graphics mid-layer and gives its 2D context
2+
// to the external renderer. CommonJS to match the rest of dance-party.
3+
4+
class ExternalDancerLayer {
5+
constructor(p5, worldW, worldH, renderer) {
6+
if (!p5 || !renderer)
7+
throw new Error('ExternalDancerLayer requires p5 and a renderer');
8+
this.p5 = p5;
9+
this.renderer = renderer;
10+
11+
this.graphics = this.p5.createGraphics(worldW, worldH);
12+
this.graphics.pixelDensity(1);
13+
14+
// Hand the renderer our mid-layer 2D context
15+
this.renderer.init(this.graphics.drawingContext);
16+
}
17+
18+
async setSource(src) {
19+
return this.renderer.setSource(src);
20+
}
21+
22+
getDurationFrames() {
23+
return (
24+
(this.renderer.getDurationFrames && this.renderer.getDurationFrames()) ||
25+
null
26+
);
27+
}
28+
29+
render(frameIndex, layout) {
30+
if (!this.graphics || !this.renderer) return;
31+
// The renderer paints directly into graphics.drawingContext
32+
this.renderer.renderFrame(frameIndex, layout);
33+
}
34+
35+
resize(worldW, worldH) {
36+
// detach old <canvas> to avoid DOM leaks
37+
const el =
38+
this.graphics &&
39+
(this.graphics.elt ||
40+
this.graphics.canvas ||
41+
this.graphics._renderer?.canvas);
42+
if (el && el.parentNode) el.parentNode.removeChild(el);
43+
if (this.p5 && Array.isArray(this.p5._elements)) {
44+
this.p5._elements = this.p5._elements.filter(e => e !== this.graphics);
45+
}
46+
47+
this.graphics = this.p5.createGraphics(worldW, worldH);
48+
this.graphics.pixelDensity(1);
49+
this.renderer.init(this.graphics.drawingContext);
50+
}
51+
52+
dispose() {
53+
try {
54+
this.renderer && this.renderer.dispose && this.renderer.dispose();
55+
} catch {}
56+
57+
const el =
58+
this.graphics &&
59+
(this.graphics.elt ||
60+
this.graphics.canvas ||
61+
this.graphics._renderer?.canvas);
62+
if (el && el.parentNode) el.parentNode.removeChild(el);
63+
if (this.p5 && Array.isArray(this.p5._elements)) {
64+
this.p5._elements = this.p5._elements.filter(e => e !== this.graphics);
65+
}
66+
67+
this.graphics = null;
68+
this.renderer = null;
69+
this.p5 = null;
70+
}
71+
}
72+
73+
module.exports = ExternalDancerLayer;

src/api.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ module.exports = class DanceAPI {
161161
setFuncContext: (type, param) => {
162162
nativeAPI.setFuncContext(type, param);
163163
},
164+
setExternalLayerSource: dancerName => {
165+
nativeAPI.setExternalLayerSource(dancerName);
166+
},
164167
};
165168
}
166169
};

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ const BackgroundEffects = require('./BackgroundEffects');
33
const ForegroundEffects = require('./ForegroundEffects');
44
const ResourceLoader = require('./ResourceLoader');
55
const constants = require('./constants');
6+
const ExternalDancerLayer = require('./ExternalDancer');
67

78
module.exports = {
89
DanceParty,
910
BackgroundEffects,
11+
ExternalDancerLayer,
1012
ForegroundEffects,
1113
ResourceLoader,
1214
constants,

src/p5.dance.interpreted.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ function runUserEvents(events) {
108108
priority: inputEvents[i].priority,
109109
func: inputEvents[i].func,
110110
eventType: eventType,
111-
param: param
111+
param: param,
112112
});
113113
}
114114
}
@@ -297,3 +297,10 @@ function everyVerseChorus(unit, func) {
297297
param: unit,
298298
});
299299
}
300+
301+
/**
302+
* Use the external render to create a "Generated Dancer" on the canvas.
303+
*/
304+
function setGeneratedDancer(dancerName) {
305+
setExternalLayerSource(dancerName);
306+
}

src/p5.dance.js

Lines changed: 106 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const replayLog = require('./replay');
77
const constants = require('./constants');
88
const modifySongData = require('./modifySongData');
99
const ResourceLoader = require('./ResourceLoader');
10+
const ExternalDancerLayer = require('./ExternalDancer');
1011

1112
function Behavior(func, id, extraArgs) {
1213
if (!extraArgs) {
@@ -54,6 +55,7 @@ module.exports = class DanceParty {
5455
// For testing: Can provide a custom resource loader class
5556
// to load fixtures and/or isolate us entirely from network activity
5657
resourceLoader = new ResourceLoader(),
58+
externalRendererFactory = () => null,
5759
}) {
5860
this.onHandleEvents = onHandleEvents;
5961
this.onInit = onInit;
@@ -155,6 +157,8 @@ module.exports = class DanceParty {
155157

156158
this.logger = logger;
157159

160+
this.createExternalRenderer = externalRendererFactory;
161+
158162
new P5(p5Inst => {
159163
this.p5_ = p5Inst;
160164
this.resourceLoader_.initWithP5(p5Inst);
@@ -320,6 +324,8 @@ module.exports = class DanceParty {
320324
backgroundEffect.reset();
321325
}
322326

327+
this.externalLayer?.setSource(null);
328+
323329
let foregroundEffect = this.getForegroundEffect();
324330
if (foregroundEffect && foregroundEffect.reset) {
325331
foregroundEffect.reset();
@@ -353,11 +359,28 @@ module.exports = class DanceParty {
353359
}
354360

355361
setup() {
356-
this.bgEffects_ = new BackgroundEffects(this.p5_, this.getEffectsInPreviewMode.bind(this), this.extraImages);
357-
this.fgEffects_ = new ForegroundEffects(this.p5_, this.getEffectsInPreviewMode.bind(this));
362+
this.bgEffects_ = new BackgroundEffects(
363+
this.p5_,
364+
this.getEffectsInPreviewMode.bind(this),
365+
this.extraImages
366+
);
367+
this.fgEffects_ = new ForegroundEffects(
368+
this.p5_,
369+
this.getEffectsInPreviewMode.bind(this)
370+
);
358371

359372
this.performanceData_.initTime = timeSinceLoad();
360373
this.onInit && this.onInit(this);
374+
375+
const externalRenderer = this.createExternalRenderer();
376+
if (externalRenderer) {
377+
this.externalLayer = new ExternalDancerLayer(
378+
this.p5_,
379+
this.p5_.width,
380+
this.p5_.height,
381+
externalRenderer
382+
);
383+
}
361384
}
362385

363386
getBackgroundEffect() {
@@ -384,7 +407,8 @@ module.exports = class DanceParty {
384407
this.analysisPosition_ = 0;
385408
this.songStartTime_ = new Date();
386409
this.loopAnalysisEvents = true;
387-
this.livePreviewStopTime = durationMs === undefined ? 0 : Date.now() + durationMs;
410+
this.livePreviewStopTime =
411+
durationMs === undefined ? 0 : Date.now() + durationMs;
388412
this.p5_.loop();
389413
}
390414

@@ -496,7 +520,6 @@ module.exports = class DanceParty {
496520
sprite.looping_move = 0;
497521
sprite.looping_frame = 0;
498522
sprite.current_move = 0;
499-
sprite.previous_move = 0; // I don't think this is used?
500523

501524
for (var i = 0; i < this.animations[costume].length; i++) {
502525
sprite.addAnimation('anim' + i, this.animations[costume][i].animation);
@@ -520,9 +543,26 @@ module.exports = class DanceParty {
520543
sprite.sinceLastFrame -= msPerFrame;
521544
sprite.looping_frame++;
522545
if (sprite.animation.looping) {
523-
sprite.animation.changeFrame(
524-
sprite.looping_frame % sprite.animation.images.length
525-
);
546+
const animationLength = sprite.animation.images.length;
547+
const currentMeasure = this.getCurrentMeasure();
548+
if (currentMeasure < 1) {
549+
sprite.earlyStart = true;
550+
const measureTick =
551+
(Math.max(0, currentMeasure) * sprite.dance_speed * 2) % 1;
552+
const measureFrame = Math.min(
553+
animationLength - 1,
554+
Math.floor(measureTick * animationLength)
555+
);
556+
sprite.animation.changeFrame(measureFrame);
557+
} else {
558+
if (!sprite.hasStarted && sprite.earlyStart) {
559+
sprite.looping_frame = 0;
560+
sprite.hasStarted = true;
561+
}
562+
sprite.animation.changeFrame(
563+
sprite.looping_frame % animationLength
564+
);
565+
}
526566
} else {
527567
sprite.animation.nextFrame();
528568
}
@@ -541,7 +581,6 @@ module.exports = class DanceParty {
541581
currentFrame === sprite.animation.getLastFrame() &&
542582
!sprite.animation.looping
543583
) {
544-
//changeMoveLR(sprite, sprite.current_move, sprite.mirroring);
545584
sprite.changeAnimation('anim' + sprite.current_move);
546585
sprite.animation.changeFrame(
547586
sprite.looping_frame % sprite.animation.images.length
@@ -686,6 +725,7 @@ module.exports = class DanceParty {
686725
if (sprite.animation.looping) {
687726
sprite.looping_frame = 0;
688727
}
728+
sprite.sinceLastFrame = 0;
689729
sprite.animation.looping = true;
690730
sprite.current_move = move;
691731
sprite.alternatingMoveInfo = undefined;
@@ -1197,8 +1237,10 @@ module.exports = class DanceParty {
11971237
// Called when executing the AI block.
11981238
ai(params) {
11991239
this.world.aiBlockCalled = true;
1200-
console.log('handle AI:', params);
1201-
if (this.contextType === constants.KEY_WENT_DOWN_EVENT_TYPE && this.contextKey) {
1240+
if (
1241+
this.contextType === constants.KEY_WENT_DOWN_EVENT_TYPE &&
1242+
this.contextKey
1243+
) {
12021244
// Note that this.contextKey is the key that was pressed to trigger this AI block, e.g., 'up', 'down',...
12031245
this.world.aiBlockContextUserEventKey = this.contextKey;
12041246
}
@@ -1263,12 +1305,14 @@ module.exports = class DanceParty {
12631305
if (!this.spriteExists_(sprite)) {
12641306
return;
12651307
}
1308+
sprite.depth = this.getAdjustedSpriteDepth(sprite);
1309+
}
12661310

1311+
getAdjustedSpriteDepth(sprite) {
12671312
// Bias scale heavily (especially since it largely hovers around 1.0) but use
12681313
// Y coordinate as the first tie-breaker and X coordinate as the second.
12691314
// (Both X and Y range from 0-399 pixels.)
1270-
sprite.depth =
1271-
10000 * sprite.scale + (100 * sprite.y) / 400 + (1 * sprite.x) / 400;
1315+
return 10000 * sprite.scale + (100 * sprite.y) / 400 + (1 * sprite.x) / 400;
12721316
}
12731317

12741318
// Behaviors
@@ -1408,7 +1452,8 @@ module.exports = class DanceParty {
14081452

14091453
for (let key of WATCHED_KEYS) {
14101454
if (this.p5_.keyWentDown(key)) {
1411-
events[constants.KEY_WENT_DOWN_EVENT_TYPE] = events[constants.KEY_WENT_DOWN_EVENT_TYPE] || {};
1455+
events[constants.KEY_WENT_DOWN_EVENT_TYPE] =
1456+
events[constants.KEY_WENT_DOWN_EVENT_TYPE] || {};
14121457
events[constants.KEY_WENT_DOWN_EVENT_TYPE][key] = true;
14131458
this.world.keysPressed.add(key);
14141459
}
@@ -1499,6 +1544,17 @@ module.exports = class DanceParty {
14991544
console.warn(message);
15001545
}
15011546

1547+
setExternalLayerSource(dancerName) {
1548+
if (!this.externalLayer) {
1549+
return;
1550+
}
1551+
const base = 'https://curriculum.code.org/media/musiclab/generate/dancers/';
1552+
const url = dancerName ? `${base}${dancerName}.json` : null;
1553+
this.externalLayer.setSource({url}).catch(err => {
1554+
console.error('Error loading external layer source', err);
1555+
});
1556+
}
1557+
15021558
draw() {
15031559
const {bpm, artist, title} = this.songMetadata_ || {};
15041560

@@ -1554,7 +1610,43 @@ module.exports = class DanceParty {
15541610
});
15551611
}
15561612

1557-
this.p5_.drawSprites();
1613+
if (!this.externalLayer) {
1614+
this.p5_.drawSprites();
1615+
} else {
1616+
// Draw sprites in two passes, before and after the external layer.
1617+
// This is done so that sprites that are larger (or lower down) on
1618+
// on the canvas appear in front of the external layer, creating an
1619+
// illusion of depth.
1620+
const sprites = this.p5_.allSprites.sort((a, b) => a.depth - b.depth);
1621+
1622+
// Mock sprite in the center of the canvas with default scale.
1623+
const mockSprite = {x: 200, y: 200, scale: 1};
1624+
const layerDepthCutoff = this.getAdjustedSpriteDepth(mockSprite);
1625+
1626+
const highDepth = sprites.filter(s => s.depth >= layerDepthCutoff);
1627+
const lowDepth = sprites.filter(s => s.depth < layerDepthCutoff);
1628+
1629+
for (const s of lowDepth) this.p5_.drawSprite(s);
1630+
1631+
const total = this.externalLayer.getDurationFrames() || 0;
1632+
if (total) {
1633+
// Align animations to a half-measure cycle, similar to dancers.
1634+
const measureTick = (this.getCurrentMeasure() * 2) % 1;
1635+
const frameIndexToDraw = Math.floor(measureTick * total);
1636+
1637+
this.externalLayer.render(frameIndexToDraw, {
1638+
mode: 'fit',
1639+
scale: 0.75,
1640+
align: {x: 'center', y: 'center'},
1641+
clearBeforeDraw: true,
1642+
});
1643+
1644+
this.p5_.image(this.externalLayer.graphics, 0, 0);
1645+
}
1646+
1647+
for (const s of highDepth) this.p5_.drawSprite(s);
1648+
}
1649+
15581650
if (this.recordReplayLog_) {
15591651
replayLog.logFrame({
15601652
bg: this.world.bg_effect,

0 commit comments

Comments
 (0)