This log chronicles the refactoring of Sergio Z’s 2023 p5.js slime sprite app
into a professionally documented Creative Cross Training showcase. The original sketch is a genuine novice
achievement: a controllable animated sprite walking on a tiled grass background, using a sprite sheet
and a custom Player class. Every improvement documented here is designed to make the
teaching version better — more readable, more portable to Java and Python, and more
aligned with professional standards.
Session 1 (this session) covers the initial prompt, its critique, and a complete analysis of the original code with a prioritized improvement list. Session 2 will implement the refactored sketch files and build the showcase page. Session 3 will create the Java and Python cross-training versions.
Sergio is a novice who wrote a p5js Processing app originally represented in
index.html. We’d like to analyze the existing code and improve it in such a way
that it represents an instructional design that we can then emulate in a Python and Java
context. For your convenience I have provided the hosting page and the scripts it references.
Let’s create sergiosUpdatedSlime.html that mimics the layout we have used
in other apps in this genre. I’ve provided mysticShield3.html as an example
to follow. Therefore we will also need sergiosUpdatedSlimeSketch.js for the p5js
file and we may need an improved Player.js file. The JavaScripts can all go in our
scripts subfolder (the originals were in our scripts0 subfolder). We can provide
sergiosUpdatedSlimeStyles.css in the styles folder. To chronicle this development,
let’s provide sergiosUpdatedSlimeChatlog.html with associated CSS and/or
JavaScript files to provide analysis, instruction and insight into the improvement of the
original app. I provided an example chatlog to follow. Images of the slime, a screen shot
of the slime, and a background of grass are provided. The grass could be used for the hero
image. Let’s begin with the chatlog entry with this prompt as the initial entry.
Act as a professional CS teacher, web developer and hiring manager in guiding us as to the
improvements we should make. With a list of improvements, we’ll start the refactoring
process. Please critique this prompt as you have done in previous chats.
This is a strong opening prompt for a multi-session project. Let me score it against the four elements and then address the specific instruction to act as teacher, web developer, and hiring manager.
Role — A
The triple persona “professional CS teacher, web developer and hiring manager” is precisely the right combination for this task. The CS teacher sees the pedagogical arc; the developer sees the technical debt; the hiring manager sets the professional standard that the refactored code should meet. All three lenses are needed, and naming them produces richer analysis than a single role would. This is one of the best role declarations in the collection.
Task — B+
The task is clear in aggregate — analyze, improve, create five files, chronicle.
One softener worth flagging: “we may need an improved Player.js file.”
A confident prompt states what it needs. After reading the original Player.js,
the answer is definitively yes: the class has dead code, undocumented magic numbers, and a
sprite-sheet animation technique that is the single most important concept in the file but
gets zero explanation. Upgrade “may need” to “will need” in
future sessions. The task would also be slightly stronger if it separated the analysis
session (this one) from the implementation session, since the two have different outputs.
The current prompt successfully initiates the analysis phase — that is enough for
a first prompt in a series.
Limits — A−
Strong. The prompt specifies folder destinations for every file, names the example to follow
(mysticShield3.html), identifies which images exist and what they are for, and
requests the chatlog as the first deliverable rather than jumping to implementation. This
ordering is exactly right: chronicle first, build second. One limit that would strengthen
the prompt: explicitly stating that the original files in scripts0/ must remain
untouched, so that the before-and-after comparison is preserved for students.
Format — B+
The format is implied through the example reference (“mimics the layout”, “I provided an example chatlog to follow”) and the request to start with the chatlog. That is good instinct. The prompt would be marginally stronger if it stated the expected output from this first session explicitly: “produce only the chatlog with the prompt, critique, analysis, and improvement list — no code yet.” Instead it lists all five files as goals, which could lead a less careful reader to attempt everything in one response.
Overall: A− — above average, well-structured for a complex multi-file project opener. The phrase “as you have done in previous chats” is a useful contextual anchor that keeps the response format consistent with established TNT practice.
sketch.js: Global Variables and Missing DeclarationskeyPress = true is assigned but never declared.
JavaScript silently creates an implicit global. This is a strict-mode error and a
professional red flag. Worse, keyPress is set in four places but
never read — it is dead code. Remove it.
idle, back, bg, and player
are all created without var.
In preload(): idle = loadImage(...) creates an implicit global.
Same for bg and back. In setup():
player = new Player() does the same. All four should be declared at the
top of the file with var alongside the other global declarations.
contentDiv is queried at module load time
(var contentDiv = document.getElementById("primaryContent"))
before the DOM is guaranteed to be ready. This works in practice because p5.js
defers sketch execution, but it is fragile. Move it into setup().
canvasMax is declared globally but initialized inside
establishCanvas(), making it appear uninitialized at the top
of the file. Either initialize it at declaration or move the constant to a named
constants section.
sketch.js: Magic Numbers (Every Single One)A hiring manager reviewing this file would count ten distinct unnamed numeric literals scattered across five functions. In the refactored version, every one of these becomes a named constant in an organized constants section at the top of the file. Here is the complete inventory:
| Value | Appears | Proposed Name | Notes |
|---|---|---|---|
1 | aspectRatio = 1 | ASPECT_RATIO | Square canvas; naming communicates intent |
600 | canvasMax = 600 | CANVAS_MAX | Maximum canvas width in pixels |
0.8 | 0.8*contentDiv.clientWidth | CANVAS_FILL_RATIO | Canvas fills 80% of its container |
300 | small-screen threshold | SMALL_SCREEN_THRESHOLD | Below this width, use minimum canvas |
200 | small-screen canvas size | SMALL_SCREEN_SIZE | Minimum canvas dimension |
60 | fr = 60 | FRAME_RATE | Target frames per second |
3 | acceleration (×4) and velocity cap (×8) | PLAYER_SPEED | The number 3 appears twelve times |
2 | width*2 in background draw | Inline comment | Doubles width to cover full canvas; no constant needed but requires a comment |
sketch.js: Missing Documentationdraw() has no layer order comment. The drawing sequence
is: background → tiled grass → player. Why does the background clear happen first?
What does player.vel.mult(0) at the end of draw() do? A numbered
painter’s-list comment answers both questions. The vel.mult(0)
line is the friction/braking mechanism — it stops the player from sliding
between frames. Without a comment it looks like a bug.
image(bg, width/2, height/2, width*2, height) is unexplained.
The width*2 parameter draws the background twice as wide as the canvas so
that the tiled pattern covers the entire area from center-anchored drawing. This is a
deliberate technique, not a mistake, and it needs a comment.
Player.js: Dead Codethis.sizeX and this.sizeY are computed every frame
but never used. Inside drawPlayer():
this.sizeX = width/30; this.sizeY = height/15; These assignments run
60 times per second and their results are discarded. This is dead code. Remove both
lines. (The sprite is sized using this.size * 1.2 instead.)
this.accel is initialized but never modified. The
constructor creates createVector(0,0) for acceleration, and
update() adds it to velocity — but since it is always zero, the addition
does nothing. Either document it as a future-ready hook (“acceleration reserved
for future physics features”) or remove it. Leaving it silently does nothing.
Player.js: The Sprite Sheet Technique (The Most Important Teaching Moment)The most instructionally valuable thing in the entire app is this line inside
drawPlayer():
image(idle, x, y, this.size*1.2, this.size*1.2,
idle.height * floor(this.frame), 0,
idle.height, idle.height);
This is the nine-parameter form of p5.js image() —
the sprite-sheet crop technique. It says: “draw the idle image at
position (x, y), sized to this.size × 1.2 on screen,
but crop from the source image starting at
x = idle.height × floor(this.frame), y = 0,
for a region idle.height wide and idle.height tall.”
This assumes the sprite sheet has square frames (width = height per frame), and that
frames are arranged left-to-right. The variable this.frame advances from 0
to 1.9 in steps of 0.05, and floor() snaps it to frame index 0 or 1 —
giving a two-frame animation. This technique is the primary CS concept the
refactored sketch teaches. In v2, it has zero comments. The refactored
Player.js will explain it completely.
image() call naming each argument, and a comment
explaining why floor(this.frame) gives the frame index while
this.frame advances continuously.
Player.js: Magic Numbers| Value | Appears | Proposed Name | Notes |
|---|---|---|---|
50 | this.size = 50 | PLAYER_SIZE | Base size of the player in pixels |
1.2 | this.size * 1.2 (×4) | SPRITE_SCALE | Renders sprite 20% larger than the hitbox |
1.9 | frame cap | ANIM_FRAME_MAX | 2 frames − 0.1 threshold to prevent out-of-bounds |
0.05 | frame increment | ANIM_SPEED | Controls animation playback rate; higher = faster |
0.09 | velocity dead zone (×2) | VEL_THRESHOLD | Snaps very small velocities to zero (prevents drift) |
When the Player class is ported to Java and Python, these constants become the primary reference for anyone translating the behavior. Named constants survive translation; magic numbers require re-discovery in every language.
Player.js: Missing Direction CasedrawPlayer() handles: moving up (vel.y < 0 && vel.x == 0
→ shows back), moving left (vel.x < 0 → flipped idle), and moving right
or stationary (vel.x >= 0 → idle). When the player moves purely downward
(vel.y > 0, vel.x == 0), it falls into the vel.x >= 0
branch and shows the right-facing idle animation. This is a design decision, not a crash,
but it should be documented with a comment: “moving down falls through to
right-facing idle — no separate down sprite in this version.”
- p5.js 1.4.2 → upgrade to 1.9.4
- Bootstrap 5.0.2 → upgrade to 5.3.3
- Template variables
myTitle,appTitle,firstNLastIare never updated from their placeholder values dateStamp = lastUpdatecreates an implicit global (missingvar)- No
rel="noopener noreferrer"on several external links - No OG meta tags, no description meta
- No
linkGuard.jsintegration - The title still reads “Template | Template”
- Declare all implicit globals with
varat the top ofsketch.js:idle,back,bg,player. - Remove
keyPressentirely — it is set but never read; dead code confuses learners. - Remove
this.sizeXandthis.sizeYfromPlayer.drawPlayer()— also dead code. - Document the sprite-sheet animation technique completely: class header,
parameter-by-parameter comment on the nine-argument
image()call, and an explanation of thefloor(this.frame)frame-index calculation. - Name every magic number — create a constants section in each file covering canvas size, speed, animation, and player dimensions.
- Add file headers to both
sketch.jsandPlayer.js. - Add a numbered painter’s-list comment to
draw()explaining the layer order and the purpose ofplayer.vel.mult(0). - Document the background tile technique — explain why
width*2is used in theimage()call. - Add function header comments to
drawPlayer(),update(),establishCanvas(), andmyResizeCanvas()explaining what each does and what coordinate context it runs in. - Add a class header comment to
Playerexplaining the lifecycle: constructed once, updated every frame, draws from a sprite sheet. - Document
this.accelas a future-ready hook or remove it. - Move
contentDivquery intosetup().
- Add direction labels to the four keyboard input blocks in
draw():// ── right ──,// ── left ──, etc. - Document the downward-movement fallback in
drawPlayer()so the reader knows the behavior is intentional. - Simplify
drawPlayer(x, y)todrawPlayer()and usethis.posdirectly — the parameters are alwaysthis.pos.x, this.pos.yand the external parameter adds noise. - Group the velocity clamping block together with a single comment rather than scattering four nearly-identical checks.
- Upgrade to Bootstrap 5.3.3 and p5.js 1.9.4.
- Add complete meta tags (description, OG, author).
- Use the TNT 2026 navbar and footer pattern from
mysticShield3.html. - Add linkGuard.js integration.
- Follow the spark-bar / hero / intro-card / showcase-card layout from the Creative Cross Training template.
When this sketch is ported to Java Processing and Python Processing, the following concepts must be visible and named in the JavaScript version so translators can find the equivalent in each language:
| Concept | JS (p5.js) | Java (Processing) | Python (Processing.py) |
|---|---|---|---|
| Sprite sheet crop | image(img,dx,dy,dw,dh,sx,sy,sw,sh) | image(img,dx,dy,dw,dh,sx,sy,sw,sh) | image(img,dx,dy,dw,dh,sx,sy,sw,sh) |
| Sprite flip (mirror) | push(); scale(-1,1); pop() | pushMatrix(); scale(-1,1); popMatrix() | push(); scale(-1,1); pop() |
| Vector-based movement | createVector() | new PVector() | No built-in; use x,y floats |
| Named constants | const NAME = value | final type NAME = value; | UPPER_CASE convention |
| Class definition | class Player { ... } | class Player { ... } | class Player: ... |
Note: Python Processing has no built-in PVector equivalent; positions and velocities become plain float variables or a simple custom class. This is itself a cross-training lesson: the same concept (velocity-based movement) is expressed differently depending on what the language provides.
I concur with our upgrade plan as specified in the chatlog. Remember the original
app (in index) was framed in an older HTML template (that was never, sadly, completely
customized). The framework for our improved app will be like that of
mysticShield3.html. Be sure to add explanatory comments in the code
during the redesign. What really needs clarification is how the slime appears to
‘breathe’ as its just sitting there. Looking at the original code, it
was nearly impossible to ‘see’ how that ‘trick’ was pulled off.
It should be clear in our refactor how it is done, given comments in the code and
explanations in the chatlog. Please initiate the refactor and add this prompt and
your observations in our chatlog.
The “breathing” is a two-frame sprite-sheet animation — the simplest possible animation loop. Here is the complete mechanism, step by step:
Step 1 — The sprite sheet has two frames, not one
slimeSprite.png is a horizontal strip containing two distinct slime
shapes placed side by side:
slimeSprite.png: ┌────────────────┬────────────────┐ │ │ │ │ frame 0 │ frame 1 │ │ (squished, │ (rounder, │ │ wider body) │ fuller body) │ │ │ │ └────────────────┴────────────────┘ srcX = 0 srcX = idle.height
Each frame is square: idle.height pixels wide and idle.height
pixels tall. The two shapes look slightly different — one is more flattened, one is
fuller. Alternating between them creates the breathing illusion.
Step 2 — The frame counter advances every update
this.frame += ANIM_SPEED; // 0.05 per frame at 60 fps
if (this.frame > ANIM_FRAME_MAX) {
this.frame = 0;
}
// cycle length: 2.0 / 0.05 = 40 frames ≈ 0.67 seconds
this.frame grows from 0.0 to 1.9 in 40 steps. The counter
never stops advancing — even when no key is pressed — keeping
the slime alive at rest.
Step 3 — floor() converts the counter to a frame index
var frameIndex = floor(this.frame); // this.frame 0.00–0.99 → frameIndex = 0 → squished shape // this.frame 1.00–1.89 → frameIndex = 1 → rounder shape
Step 4 — The nine-parameter image() crops the right frame
image(idle,
this.pos.x, this.pos.y, // destination: where on the canvas
displaySize, displaySize, // destination: draw size
srcX, 0, // source: crop offset (0 or idle.height)
idle.height, idle.height); // source: one square frame
This nine-parameter form of image() is identical in JS p5.js, Java
Processing, and Python Processing — it is the universal sprite-sheet technique,
making the cross-training port a direct translation.
Why it was invisible in the original
The original contains 1.9, 0.05, floor(this.frame),
and idle.height * floor(this.frame) with zero comments. The refactored
Player.js opens with a step-by-step diagram and names every constant
(ANIM_FRAME_MAX, ANIM_SPEED, SPRITE_SCALE)
so the breathing trick is comprehensible before reaching the actual code.
The showcase page has two independent code display panels. The primary panel shows
sergiosUpdatedSlimeSketch.js (canvas setup, keyboard input, draw order).
The secondary panel shows Player.js (the breathing trick, sprite-sheet crop,
named constants). Separating them makes each concept independently findable.
constrain() Replaces Eight if-BlocksThe original uses four separate if(vel > 3) blocks to clamp velocity.
The refactored version uses constrain(player.vel.x, -PLAYER_SPEED, PLAYER_SPEED).
Two lines replace eight. constrain() is identical in p5.js, Java Processing,
and Python Processing, making this a direct cross-training translation.
drawPlayer(x, y) → drawPlayer()The original always called this.drawPlayer(this.pos.x, this.pos.y) —
the external parameters were always this.pos. The refactored version uses
this.pos directly inside the method. Better encapsulation: the Player knows
where it is; callers do not need to tell it.
| File | Purpose | Key feature |
|---|---|---|
scripts/sergiosUpdatedSlimeSketch.js |
Main sketch | 8 named constants; painter’s-list draw() comment; braking explained |
scripts/Player.js |
Player class | Step-by-step breathing-trick diagram in file header; 5 named constants; dead code removed |
scripts/slimeScripts.js |
Code display utilities | buildCodeDisplay(), copyRaw(), JS syntax highlighting |
styles/sergiosUpdatedSlimeStyles.css |
Showcase styles | Slime-green palette; greenbar code panel; page nav; grass hero |
sergiosUpdatedSlime.html |
Showcase page | Modelled on mysticShield3.html; Bootstrap 5.3.3; p5.js 1.9.4 |
Originals preserved untouched in scripts0/ and sergiosSlimeOrig.html.
On page launch, I don’t see anything other than the “Live Sketch” notification and the move instructions. It looks as if the canvas hasn’t loaded?
Root cause: establishCanvas() sizes the canvas
from contentDiv.clientWidth, where contentDiv is
#sketchContentDiv. At the moment setup() runs, that
div is still empty — the canvas has not been attached yet. An empty
div inside a display:flex; align-items:center column
reports clientWidth = 0. So:
var myWidth = CANVAS_FILL_RATIO * contentDiv.clientWidth; // 0.85 × 0 = 0 createCanvas(myWidth, myWidth); // ← createCanvas(0, 0) — silent, invisible
p5.js accepts a 0×0 canvas without complaint. The sketch then calls
draw() 60 times per second into a canvas too small to see. No error,
no warning, nothing on screen.
Why the debug log confirmed it: the console.log
inside establishCanvas() was already printing
contentDiv.clientWidth = 0 and myWidth = 0 — the
problem was right there in the output, requiring no additional instrumentation.
#sketchContentDiv a Non-Zero WidthThe root cause is that the div has no intrinsic width before the canvas is inserted.
Adding width: 100% to the CSS rule tells the browser to fill the available
column width regardless of whether the div has any content yet:
/* sergiosUpdatedSlimeStyles.css */
#sketchContentDiv { width: 100%; text-align: center; }
After this fix, clientWidth returns the true column width when
setup() runs, and the canvas sizes correctly on the first frame. This is the
primary fix — the one that actually resolves the problem.
establishCanvas(): Fallback to CANVAS_MAXIf the CSS is somehow not applied (cold load, cached file, browser quirk), the guard
catches the zero before it reaches createCanvas():
var myWidth = CANVAS_FILL_RATIO * contentDiv.clientWidth;
// Guard: if clientWidth is still 0, fall back to a known-good size
if (!myWidth || myWidth < SMALL_SCREEN_SIZE) { myWidth = CANVAS_MAX; }
This does not go into myResizeCanvas() — by the time a resize event
fires, the canvas is already present and clientWidth is non-zero. The guard
is only needed at first creation.
imageMode(CENTER) Before the Background DrawThis one is independent of the canvas-size bug but surfaced during the same debugging
pass. The background was being drawn without setting imageMode explicitly.
On the very first frame, imageMode defaults to CORNER; on every subsequent
frame, drawPlayer() had already set it to CENTER. The result
was that the grass background was positioned differently on frame 1 than on all later frames —
a one-frame inconsistency that most users would never notice but which violated the
“explicit over implicit” principle of the teaching edition:
// draw() — Layer 2 imageMode(CENTER); // explicit — drawPlayer() also changes this image(bg, width / 2, height / 2, width * 2, height);
Named and intentional is always better than “happens to work because of call order.”
Give the container width: 100% in CSS and add a JavaScript
guard that falls back to a known-good size. Neither alone is fragile; together they
are robust:
/* CSS */
#sketchContentDiv { width: 100%; }
/* JS — in establishCanvas() */
var myWidth = CANVAS_FILL_RATIO * contentDiv.clientWidth;
if (!myWidth || myWidth < SMALL_SCREEN_SIZE) { myWidth = CANVAS_MAX; }
This pattern should be considered standard in any TNT sketch that uses a
container-relative canvas size. The original slime sketch did not need it because
the container was a full-page <main> element with a natural width;
the refactored version runs inside a Bootstrap flex column, which is the context
where the bug appears.
I notice in the app that the left/right arrow keys move the slime as expected but the up/down keys not only move the slime but also the webpage itself! I think removing that default behavior of intra-web page navigation is a bad move, but we could restrict it only when the mouse is within the canvas area. That’s my proposal. What is your critique of it? Are there better solutions?
Critique of the mouse-over proposal
The proposal is reasonable in intent but uses the wrong signal. Mouse position and keyboard intent are independent variables. Consider the typical play session:
- User clicks the canvas to start, then moves their hand to the keyboard.
- While pressing ↑ to move the slime, the mouse drifts slightly to the left — off the canvas panel and onto the code panel.
- At that moment, the mouse-over flag goes
false, arrow-key scroll resumes, and the page jumps while the slime also moves.
The edge case is subtle but real, especially on smaller screens where the code panel is narrow. The mouse-over approach works for careful users but is not robust for casual play.
The three available strategies
There is a spectrum of solutions, from blunt to precise:
preventDefault() (the nuclear option)window.addEventListener('keydown', function(e) {
if (e.keyCode >= 37 && e.keyCode <= 40) {
e.preventDefault();
}
});
Pro: three lines, completely eliminates the scroll conflict.
Con: arrow keys can no longer scroll the page at all, even
before the user has interacted with the sketch. This is the problem klp identified —
it removes a natural navigation affordance from the page.
Verdict: appropriate for full-page games; wrong for a showcase page where the sketch shares space with code panels and a footer.
var mouseOverCanvas = false;
// in setup():
myCanvas.elt.addEventListener('mouseover', function() { mouseOverCanvas = true; });
myCanvas.elt.addEventListener('mouseout', function() { mouseOverCanvas = false; });
window.addEventListener('keydown', function(e) {
if (mouseOverCanvas && e.keyCode >= 37 && e.keyCode <= 40) {
e.preventDefault();
}
});
Pro: no extra HTML attribute needed; intuitive “hover to
activate” metaphor; preserves page scrolling until mouse enters canvas.
Con: mouse position and keyboard focus are independent — the user
can hover off-canvas while still holding an arrow key, causing a simultaneous
scroll + slime-move. Also: a keyboard-only user cannot play without triggering
scroll regardless of what the mouse does.
Verdict: works for most casual use; has a real edge case under normal play conditions. Acceptable but not robust.
// In sergiosUpdatedSlimeSketch.js — end of setup():
contentDiv.setAttribute('tabindex', '0');
contentDiv.style.outline = 'none'; // suppress focus ring (canvas has its own border)
// Auto-focus when user clicks the sketch area
myCanvas.elt.addEventListener('click', function() { contentDiv.focus(); });
// Prevent scroll only while the sketch container has keyboard focus
contentDiv.addEventListener('keydown', function(e) {
if (e.keyCode >= 37 && e.keyCode <= 40) {
e.preventDefault();
}
});
How it works: giving #sketchContentDiv a
tabindex makes it a focusable element. When the user clicks the canvas,
contentDiv.focus() is called, transferring keyboard focus there. The
keydown listener on contentDiv then catches arrow keys
and cancels their default scroll. When the user clicks anywhere else on the page,
focus leaves contentDiv and normal arrow-key scrolling resumes.
Why this is right: focus is the correct semantic signal for
“this element currently owns keyboard input.” It is how the browser
already distinguishes between a text field and the rest of the page. p5.js’s
keyIsDown() listens on the window object and is unaffected
— the sketch still receives all key events. The preventDefault() call only
cancels the browser’s scroll action, not the keypress itself.
Pro: precise, predictable, no edge cases, semantically correct,
accessible (keyboard-only users can Tab to the canvas to focus it).
Con: requires a one-time click to “activate” the sketch;
a first-time visitor might not realize this. A subtle “click to play”
hint in the canvas caption addresses this.
Verdict: the industry standard for browser-embedded games. This is how every serious browser game handles the scroll conflict.
The user’s proposal correctly identifies that unconditional prevention is too broad. The intuition to scope the prevention to the canvas area is exactly right — the implementation just needs to use focus rather than mouse position. Focus answers the question “does this element currently own keyboard input?” Mouse position answers the question “where is the pointer right now?” These are different questions, and only the first one is the right condition for intercepting keyboard events.
| File | Change |
|---|---|
scripts/sergiosUpdatedSlimeSketch.js |
At end of setup(): give contentDiv a
tabindex, attach a click listener to auto-focus
it, and attach a keydown listener to cancel arrow-key scroll
while focused. |
sergiosUpdatedSlime.html |
Update canvas caption: add “Click the sketch first” to set user expectation. |
setup()// ── Focus-based arrow key fix
// Attaching keydown to contentDiv means preventDefault() only fires
// while the sketch has keyboard focus — leaving page scroll intact elsewhere.
contentDiv.setAttribute('tabindex', '0');
contentDiv.style.outline = 'none'; // canvas border already provides visual focus cue
myCanvas.elt.addEventListener('click', function() { contentDiv.focus(); });
contentDiv.addEventListener('keydown', function(e) {
if (e.keyCode >= 37 && e.keyCode <= 40) { e.preventDefault(); }
});
The outline: none suppresses the browser’s default focus
ring (a dotted or colored border that appears when a tabindex
element gains focus). The canvas already has its own visual boundary inside
the panel, so the default ring would be redundant and visually jarring.
keyCode 37–40?Browser key codes 37–40 are
the four arrow keys: 37 = ←, 38 = ↑, 39 = →, 40 = ↓.
WASD keys (65, 87, 68, 83) are not in this range and never trigger
scroll, so they do not need to be in the condition.
The range check (>= 37 && <= 40) is cleaner than
listing four separate values and matches the four adjacent key codes exactly.
In the hero heading, we have 2 icons. Let’s replace them with the
slimeOnGrass.gif file for a more thematic incorporation.
Please critique this choice.
The case for the GIF
The two emojis (🪸 + 🟢)
were decorative placeholders. They communicated “green creature” in the
abstract. slimeOnGrass.gif communicates “this specific character,
in this specific setting” — which is exactly what the page is about.
Replacing generic emoji with the actual subject is a straightforward upgrade:
it creates visual continuity between the hero and the live canvas below it.
A second advantage: emoji render inconsistently across operating systems and browsers. The same codepoint looks noticeably different on Windows, macOS, Android, and iOS. The GIF is pixel-identical everywhere.
The one implementation detail that must be right
Pixel art scaled by the browser’s default algorithm becomes blurry. The browser interpolates between pixels to smooth the image, which is the opposite of what pixel art wants. The fix is one CSS property:
image-rendering: pixelated;
This instructs the browser to use nearest-neighbour scaling — each
source pixel becomes a sharp rectangle of destination pixels, preserving the
crisp pixel-art look at any size. Without it, scaling the GIF to
height: 1.15em inside a large heading would produce a soft,
smeared result.
Concern: animation in a heading
Animated elements near heading text can split attention and make the text harder to read on first scan. This is a real risk on pages where reading comprehension is the primary goal (documentation, news articles). On a creative coding showcase page, the animation is the subject — the same reasoning that makes a movie poster appropriate on a cinema home page. The risk is low here.
Verdict: A− — the right choice for this genre
of page. The emoji were always approximate; the character GIF is exact. The only
required implementation guard is image-rendering: pixelated.
img in h1<h1 class="hero-title">
<img src="images/slimeOnGrass.gif"
alt="animated slime on grass"
style="height:1.15em; vertical-align:middle;
margin-right:0.3em; image-rendering:pixelated;">
Sergio’s Slime:<br>Teaching Edition
</h1>
height: 1.15em scales the image relative to the heading’s computed
font size, so it stays proportional as the clamp() font size adjusts
across viewport widths. vertical-align: middle aligns it with the
cap-height of the first line of text.
Since there are 2 files rather than 1, how can we import them both into the p5js editor for implementation there? We’ll also need a way to download the background image as well, and provide a default background in case users fail or cannot upload the image properly.
Challenge 1 — Two JavaScript files in the p5.js editor
The p5.js online editor supports multi-file projects. Every .js file
added to the project is included in the same auto-generated index.html
and runs in the same global scope. This means the Player class in a
separate Player.js tab is automatically available to
sketch.js with no import statement required —
exactly the same way the two files work when loaded by the showcase page.
The only requirement: create Player.js before running the
sketch. The editor adds new files above sketch.js in the
index.html include order, so creating Player.js first
guarantees the class is defined before the sketch calls new Player().
Challenge 2 — Three images
The sketch loads three images at startup via preload():
slimeSprite.png— the two-frame sprite sheet (required for animation)slimeBack.png— the back-facing sprite (required for upward movement)Grass_Sample.png— tiled grass background (cosmetic — can fall back to a color)
All three must be uploaded into an images/ folder in the editor project.
The existing paths in the sketch ("images/slimeSprite.png" etc.)
already match this structure and require no changes.
Challenge 3 — Background fallback when the image does not load
If Grass_Sample.png fails to upload, the unchecked
image(bg, ...) call draws a blank image silently — the canvas just
shows the light-gray clear color. Adding a null guard substitutes a dark-green background
that reads as “grass” while keeping the slime fully playable. This is the
same pattern used in Interactive Mystic Shield for its optional background image.
draw()// Layer 2: grass background (fallback if Grass_Sample.png is not loaded)
imageMode(CENTER);
if (bg) {
image(bg, width / 2, height / 2, width * 2, height);
} else {
background(120, 60, 30); // HSB dark-green — approximates grass palette
}
In p5.js, a failed loadImage() produces a blank p5.Image
object (not null), so the guard uses if (bg) which evaluates
true for a valid loaded image and false for the blank fallback object. The sprite images
(slimeSprite.png, slimeBack.png) are handled the same way by
p5.js — a failed load produces an invisible blank sprite rather than a crash,
so they require no additional guard.
- Open editor.p5js.org. Click Sketch Files (folder icon, top-left panel).
- Click + New File. Name it exactly
Player.js. Paste the fullPlayer.jssource into it. — Create this before pasting sketch.js so it appears first in the include order. - Select
sketch.js(the default tab). Paste the fullsergiosUpdatedSlimeSketch.jssource into it. - In Sketch Files, click the folder icon to create a folder named
images. UploadGrass_Sample.png,slimeSprite.png, andslimeBack.pnginto it. - Click Run ▶. The slime appears on a grass background. If the grass image is missing, a solid dark-green background appears automatically — gameplay is fully intact.
The instructions were very clear on how to load the assets into p5js. I did so, but got this error: “The preload() function has been removed in p5.js 2.0. Please load assets in setup() using async / await keywords or callbacks instead.”
Issue 1 — preload() removed in p5.js 2.0
preload() was the p5.js 1.x mechanism for blocking sketch execution
until assets were loaded. p5.js 2.0 replaces it with JavaScript’s native
async/await pattern. The fix is to remove preload() entirely
and add await before each loadImage() call inside an
async version of setup(). This pattern works in
both 1.9.x and 2.x, so the showcase page (pinned to 1.9.4) is unaffected.
Issue 2 — contentDiv is null in the editor
document.getElementById("sketchContentDiv") returns
null in the p5.js editor because that div only exists on the TNT
showcase page. Every subsequent line that calls methods on contentDiv
— contentDiv.clientWidth, contentDiv.setAttribute(),
etc. — throws a TypeError. The fix is a null guard:
if (contentDiv) { ... } around every block that uses it.
Issue 3 — myCanvas.parent(‘slimeCanvas’) targets a non-existent div
The #slimeCanvas div is part of the TNT showcase HTML. It does not
exist in the p5.js editor’s default index.html. Calling
.parent() on a non-existent element throws silently in some browsers
but may cause a canvas-placement failure. The fix is an existence check:
if (document.getElementById(‘slimeCanvas’)) { ... }
Issue 4 — vel.mult(0) may not modify in place in 2.x
In p5.js 2.0, some vector instance methods no longer modify the vector in place
— they return a new vector instead. player.vel.mult(0) was the
braking mechanism; if it only returns a new zero vector without modifying
player.vel, the slime never stops. The safe cross-version replacement
is direct property assignment: player.vel.x = 0; player.vel.y = 0;
sergiosUpdatedSlimeSketch.js| Issue | Old code | New code |
|---|---|---|
| preload() removed | function preload() { idle = loadImage(...); }function setup() { ... } |
async function setup() { idle = await loadImage(...);} |
| null contentDiv | contentDiv.clientWidth (crashes) |
if (contentDiv) { ... } |
| missing #slimeCanvas | myCanvas.parent(‘slimeCanvas’) (no-op or error) |
if (document.getElementById(‘slimeCanvas’)) { ... } |
| vel.mult() in-place | player.vel.mult(0); |
player.vel.x = 0; player.vel.y = 0; |
I made the changes and uploaded as suggested. I double-checked the file names
and all looked good but it said it couldn’t find the Player.js
info. Error: ReferenceError: Player is not defined at setup (/sketch.js:98).
The background image did eventually load though.
What changed in p5.js 2.x
In p5.js 1.x, all files added to the online editor were included as plain
<script> tags. Plain scripts share the same global scope, so
a class defined in Player.js was automatically visible as a bare name
(Player) in sketch.js.
In p5.js 2.x, the editor wraps or isolates sketch files in a way that prevents
class and function declarations from becoming globals automatically. The
Player class exists inside Player.js’s scope but
is not promoted to window.Player, so sketch.js cannot
find it by bare name.
Why the background image loaded but Player did not
The await loadImage() calls return p5.js Image objects — they
come from the p5.js runtime, which is accessible across scopes. The
Player class is user-defined code that only exists in the scope where
Player.js ran. If that scope is isolated, the class never reaches
sketch.js.
The fix: one line at the end of Player.js
Explicitly attaching the class to window bypasses scope isolation.
window is always the true global object, accessible from any scope:
// At the very end of Player.js:
if (typeof window !== 'undefined') { window.Player = Player; }
After this line, window.Player is the class. In any regular script
(including sketch.js), bare Player resolves to
window.Player, so new Player() works correctly.
typeof window !== ‘undefined’?The guard makes the line safe to run in any JavaScript environment:
window exists in browsers but not in Node.js or Processing.py.
Since the sketch only runs in a browser, the guard always evaluates true here —
it is a defensive pattern that costs nothing and prevents a crash if the file is
ever run in a non-browser context (e.g., a test suite or server-side renderer).
sketch.jsIf the window export feels like a workaround, the other option is
to paste the full Player.js source at the bottom of
sketch.js and use only one file in the editor. The class must appear
after all the sketch code (or at least after any code that uses
class Player but before setup() runs) to satisfy the
class declaration timing rule. This is the simplest possible approach and is
sometimes the right choice for novices who are not yet comfortable with multi-file
projects.
window.Player Didn’t Work Either — Use One FileI was hopeful that would work, but after re-uploading Player.js
with window.Player = Player at the end, I got the exact same error:
ReferenceError: Player is not defined at setup (/sketch.js:98).
Two failed attempts from two different angles points to one conclusion:
p5.js 2.x loads sketch.js as a type="module"
script. ES modules are strict-mode and have their own scope. Inside a module,
bare identifiers (like Player) do not resolve through
window even if window.Player is set. You would have
to write new window.Player() explicitly — which is the same
as admitting the two-file approach no longer works cleanly.
The definitive solution for the p5.js 2.x editor is to use one file.
Paste the entire Player.js class at the bottom of sketch.js
before running. This eliminates the cross-file scope problem entirely: the class
and the sketch are in the same module and see each other without any imports.
Important: the Player class definition must appear before setup()
is called, not before it is defined. Since p5.js calls
setup() asynchronously after the script loads, placing the class
at the very end of the file (after myResizeCanvas()) is fine.
- Download sketch.js and Player.js from the showcase page.
- Open editor.p5js.org.
Leave the project as a single-file project (no need to add
Player.jsas a separate file). - Open
sketch.jsin a text editor locally. OpenPlayer.jsin a text editor locally. Copy everything inPlayer.jsand paste it at the very bottom ofsketch.js(after the last line ofmyResizeCanvas()). - Paste the combined content into the editor’s single
sketch.jstab. - In Sketch Files, create an
imagesfolder and uploadGrass_Sample.png,slimeSprite.png, andslimeBack.png. - Click Run ▶. The slime should appear.
The TNT showcase page loads scripts via regular (non-module)
<script> tags. In that context, Player.js and sketch.js
share the same global scope and the two-file structure works perfectly.
The p5.js editor’s module-based runner is a different environment.
Two files is the right design for the teaching showcase; one file is the
pragmatic workaround for the editor’s module isolation.
Looking into the future, I’m wondering if we will be successful creating a working Python and Java version, given the difficulties we’ve had here. Do you anticipate this endeavor to be a fool’s errand, or do you think we should proceed full-steam-ahead? Please list your considerations and concerns.
Not a fool’s errand. Proceed.
The verdict first: both Java and Python versions are achievable. The browser-compatibility rabbit holes that consumed Sessions 5–6 cannot happen in the Processing IDE. Every remaining challenge is a code translation problem, and they are all catalogued and solvable. Here is the honest ledger.
What is already done and transfers directly
The refactoring work in Sessions 1–2 was the hard part for any language
port. Every concept is now named, documented, and isolated. The nine-parameter
image() sprite-sheet crop is identical in Java Processing,
Python Processing, and p5.js — same parameter order, same coordinate system.
The breathing trick, the frame counter, the braking mechanism, the boundary
clamping: all directly translatable. The refactored sketch is already written
at the level of abstraction a translator needs.
| Item | Status | Notes |
|---|---|---|
| Multi-file sketch | ✓ Native | Each .pde tab in the Processing IDE compiles into the same program. Player in its own tab is automatically visible to the main sketch — no import needed. |
| Sprite-sheet crop | ✓ Identical | image(img,dx,dy,dw,dh,sx,sy,sw,sh) is the same API. No changes. |
| Vector movement | ✓ Direct | createVector() → new PVector(). Same fields (.x, .y), same math. |
| push/pop + scale flip | ✓ Rename only | push() / pop() → pushMatrix() / popMatrix(). One word change. |
| Named constants | ✓ Cleaner | const PLAYER_SPEED = 3 → final int PLAYER_SPEED = 3;. Java enforces immutability at compile time. |
| Keyboard input | ⚠ Adaptation | Java Processing has no keyIsDown() polling. Multi-key detection requires a HashSet<Integer> updated in keyPressed() and keyReleased(). This is a well-known pattern — documented, predictable, ~10 lines. |
| Canvas sizing | ✓ Simpler | Processing window IS the canvas. Replace establishCanvas() with size(480, 480) in setup(). All the Bootstrap/clientWidth code disappears. |
| colorMode(HSB) | ✓ Identical | colorMode(HSB, 360, 100, 100) works the same in Java Processing. |
| async / preload | ✓ N/A | Java Processing loads images synchronously with loadImage() in setup(). No async complexity. |
Verdict: the Java port is the most straightforward of the three. The only non-trivial adaptation is keyboard polling, which has a textbook solution. Estimated new code: ~10 lines for the key-tracking set; everything else is syntax translation.
| Item | Status | Notes |
|---|---|---|
| Multi-file sketch | ✓ Native | Python mode in the Processing IDE works exactly like Java mode: all .py files in the sketch share one scope. No module isolation. |
| Sprite-sheet crop | ✓ Identical | Same 9-param image() signature. No changes. |
| Vector movement | ⚠ Adaptation | Python Processing has PVector accessible via Java interop, but the ergonomics are clunky. The cleaner teaching approach: replace vel, pos, accel with plain float x/y pairs. This is itself a cross-training lesson — “when a language has no built-in vector, you build one from components.” |
| Class definition | ✓ Direct | class Player: with def __init__(self):. Python classes are more concise than Java equivalents. this. → self. |
| Named constants | ✓ Convention | const PLAYER_SIZE = 50 → just PLAYER_SIZE = 50 at module level. Python uses UPPER_CASE as the convention signal for constants. |
| push/pop + scale flip | ✓ Same | Python Processing uses pushMatrix() / popMatrix() (same as Java). Or with pushMatrix(): context manager in some versions. |
| Keyboard input | ⚠ Adaptation | Same challenge as Java: no keyIsDown() polling. Needs a set of held keys updated in keyPressed() / keyReleased(). |
| Canvas sizing | ✓ Simpler | size(480, 480) in setup(). All web-layout code disappears. |
| async / preload | ✓ N/A | No async in Processing.py. loadImage() is synchronous. Simple. |
| imageMode / colorMode | ✓ Same API | Both work identically in Python mode. |
Verdict: achievable, but Python requires the most structural rethinking
because of the vector situation. That rethinking is itself pedagogically valuable —
it shows students that the concept (velocity-based movement) survives even when
the language tool (PVector) does not. The keyboard adaptation is the same
as Java. No new unknowns.
Every problem in Sessions 5–6 came from one source: the p5.js online editor is a web application running JavaScript in a browser, subject to ES module scoping, version drift, and a DOM that differs from the TNT showcase page. Java Processing and Python Processing run in the Processing desktop IDE, which is:
- Stable: API changes are not deployed without user consent.
- Unified scope: All
.pde/.pyfiles in a sketch compile together. No module isolation, no import ceremony. - Synchronous: No async/await, no preload() removal, no version incompatibility to navigate.
- Local: No browser, no CDN, no network requests for the library itself.
In his original work, novice Sergio used the online tool, Piskel, at
piskelapp.com.
I’ve never used it and it would be wonderful if you could create a cheat sheet
for using it, as we did for JDoodle. Assume we know nothing and help us build a sprite
from scratch. Store the cheat sheet in our resources/ subfolder and use
the same hero image as the slime upgrade. Use the slime image too as a way to capture
interest.
What Piskel is
Piskel is a free, browser-based pixel art editor purpose-built for sprite sheets and animated sprites. No download, no account required. You draw on a pixel grid, add frames for animation, preview the loop, and export as a PNG spritesheet or an animated GIF. It is exactly the kind of tool that a novice like Sergio would use to build game art without any prior drawing software experience.
What the cheat sheet covers
The cheat sheet (resources/piskelCheatSheet.html) is structured
identically to the JDoodle cheat sheet: green-themed hero, numbered quick-start
steps, an interface guide, a drawing tools reference, and an honest assessment of
where Piskel excels and where other tools are better. The unique feature is a
from-scratch “Build the Slime” tutorial —
a step-by-step walkthrough that produces a two-frame breathing sprite using the
exact colors and technique Sergio used, then exports it as the sprite sheet format
that the 9-parameter image() call expects.
| Section | Content |
|---|---|
| Quick Start — 7 steps | Navigate to Piskel, set canvas size, zoom in, pick a color, draw with the Pencil, add a second frame (the breathing trick), export the spritesheet. |
| Interface Guide | Canvas, Tool Palette, Color Panel, Preview Window, Frames Panel, Top Menu Bar — one labeled description for each. |
| Drawing Tools | Pencil (P), Eraser (E), Paint Bucket (B), Eyedropper (I), Rectangle/Circle (R/C), Move (M), Symmetry — with keyboard shortcuts for each. |
| Build the Slime | Color palette (4 hex codes), Frame 1 steps (outline, fill, highlight, eyes), Frame 2 steps (duplicate and modify for breathing), pre-export checklist. |
| Export Guide | Spritesheet PNG (the Processing format), single-frame PNG, animated GIF, .piskel save file. Includes the p5.js code snippet showing the 9-param image() call with the spritesheet. |
| Tips & Gotchas | Enable the grid before drawing; plan dimensions first; Onion Skin overlay; browser storage is not permanent; spritesheet must be horizontal; transparent ≠ white. |
| Honest Assessment | Use Piskel for small sprites, simple animation, beginner-friendly work. Not for large sprites, complex layers, or professional palette management. |
- Hero background: the grass background from
Grass_Sample.pngwith a dark green gradient overlay — same visual DNA as the slime showcase page. - Hero heading image:
slime-transparent.gifnext to the heading text, same treatment as the showcase and chatlog pages. - Color theme: slime green throughout (
#5CB85Cprimary,#2E7D32dark) instead of JDoodle’s orange. - Code snippet: the p5.js spritesheet loading code uses the dark green terminal style from the showcase page, making it visually consistent.
- Also See bar: links directly to Piskel, the Slime Showcase, the Slime Chatlog, the Resources page, and the JDoodle Cheat Sheet.
sergiosUpdatedSlimeJavaSketch.pdeWe have successfully created a page featuring Sergio’s slime in
JavaScript with p5js. Per your recommendation, let’s create the Java
version: sergiosUpdatedSlimeJavaSketch.pde in the scripts folder
of this app. Let’s create it and I’ll test it in the Processing
app before we try to integrate it into our cross training page. Please add
explanatory comments in the code and let’s discuss this development
in our chatlog.
HashSet<Integer> Replaces keyIsDown()This is the most significant logic change. p5.js provides keyIsDown(n)
— a polling function you can call anywhere in draw() to ask whether
key n is currently held. Java Processing has no such function. It fires
keyPressed() and keyReleased() events, but these events do
not give you a way to query key state mid-frame.
The standard Java solution is a HashSet<Integer> that acts as
a snapshot of currently held keys:
HashSet<Integer> heldKeys = new HashSet<Integer>();
void keyPressed() { heldKeys.add(keyCode); }
void keyReleased() { heldKeys.remove(keyCode); }
// In draw() — exact equivalent of p5.js keyIsDown():
if (heldKeys.contains(KEY_D) || heldKeys.contains(RIGHT)) { ... }
The key codes are identical between JavaScript and Java. Arrow key codes (LEFT=37, RIGHT=39, UP=38, DOWN=40) are the same in both environments. Letter keys use their uppercase ASCII values (A=65, D=68, W=87, S=83) in both. The only code that changes is the mechanism for querying them.
image(): A Course Correction from Session 7Session 7 stated: “Sprite-sheet crop: image(img,dx,dy,dw,dh,sx,sy,sw,sh)
is the same API. No changes.” This turns out to be incorrect. The destination
parameters are the same. The source parameters are not.
| Language | Source region specified as… |
|---|---|
| p5.js | sx, sy, sw, sh — top-left corner + width/height |
| Java Processing | u1, v1, u2, v2 — top-left corner + bottom-right corner |
The translation is one extra calculation: u2 = sx + sw. For this sketch,
where each frame is idle.height pixels wide:
// p5.js (offset + SIZE):
image(idle, pos.x, pos.y, displaySize, displaySize,
srcX, 0, idle.height, idle.height);
// Java Processing (top-left + BOTTOM-RIGHT CORNER):
int srcX2 = srcX + idle.height; // one extra line
image(idle, pos.x, pos.y, displaySize, displaySize,
srcX, 0, srcX2, idle.height);
This difference is invisible when the frame is cropped from the very top-left of the
sheet (srcX=0), because u2 = 0 + idle.height = idle.height and the original
sw = idle.height look the same. The bug only appears for frame 1 (srcX > 0),
where p5.js’s sw = idle.height gives the correct width but Java’s
u2 = idle.height would also point to the boundary between frames — wrong.
Why it matters for cross-training: students porting code between
p5.js and Java Processing could silently produce a broken animation that shows the correct
sprite for frame 0 and the wrong region for frame 1. The breathing would still appear to
“work” on close inspection but the crop coordinates would be off by exactly
srcX pixels. This is the kind of subtle off-by-one that is very hard to
diagnose without knowing the convention difference.
pushMatrix() / popMatrix() vs push() / pop()The left-facing mirror uses a coordinate flip: scale(-1, 1) combined with
a negated x position. The save/restore pair that wraps this flip is a single word change:
// p5.js: push(); scale(-1, 1); image(idle, -this.pos.x, this.pos.y, ...); pop(); // Java Processing: pushMatrix(); scale(-1, 1); image(idle, -pos.x, pos.y, ...); popMatrix();
The behavioral difference is scope: p5.js push() saves both
the transformation matrix and the drawing style (including imageMode).
Java pushMatrix() saves only the matrix. This is fine here because
drawPlayer() always sets imageMode(CENTER) at its own top —
the explicit approach means the code is correct regardless of which save/restore semantics
are in play.
sketchPath() TrickJava Processing’s loadImage() looks for files in a data/
subfolder by default. The slime images live at images/, one level above the
scripts/ folder. Using sketchPath("../images/...") constructs an
absolute path from the sketch’s own directory and navigates up one level:
// sketchPath() returns the absolute path to this .pde file's folder.
// Appending "../images/..." navigates up to the shared images/ folder.
idle = loadImage(sketchPath("../images/slimeSprite.png"));
This keeps the images in one location and avoids duplicating them into a
data/ subfolder. If the relative navigation ever fails (some systems
handle .. differently), the fallback is to copy the three image files
into scripts/data/ and use plain filenames.
Teaching note: this asset-loading simplification is one of the genuine advantages of the desktop IDE over a browser environment. No CDN, no CORS headers, no asset server — just a path on disk.
In Java Processing, a class defined in the same .pde file as the main
sketch is compiled as an inner class of the sketch’s generated
PApplet subclass. This means the Player class can directly
access all top-level constants (PLAYER_SIZE, ANIM_SPEED, etc.)
and global images (idle, back) without any parameters
or imports — the same reason the JavaScript version could access globals
declared in sketch.js from inside Player.js.
// p5.js: constants declared in sketch.js, accessed in Player.js (shared global scope) const PLAYER_SIZE = 50; // Java: final fields at sketch level, accessed inside Player class (inner class scope) final float PLAYER_SIZE = 50; // Player() constructor can read PLAYER_SIZE directly — no parameter needed
The cross-training lesson: both versions rely on a shared outer scope, but the
mechanism differs. In JavaScript (plain script tags) it is the window
object. In Java Processing it is the inner-class/outer-class relationship.
In a future refactoring, both could be given constructor parameters instead
— which would make each version more self-contained and testable.
| Feature | p5.js | Java Processing | Change |
|---|---|---|---|
| Velocity math | vel.x += PLAYER_SPEED |
vel.x += PLAYER_SPEED |
None |
| Velocity clamping | constrain(vel.x, -SPEED, SPEED) |
constrain(vel.x, -SPEED, SPEED) |
None |
| Braking | vel.x = 0; vel.y = 0; |
vel.x = 0; vel.y = 0; |
None |
| Frame counter / breathing trick | frame += ANIM_SPEED; floor(frame) |
frame += ANIM_SPEED; (int)frame |
Cast syntax only |
| Boundary checking | player.pos.x <= width - size/2 |
player.pos.x <= width - size/2 |
None |
| Direction logic in drawPlayer | if (vel.y < 0 && vel.x === 0) |
if (vel.y < 0 && vel.x == 0) |
Triple-equals removed |
| colorMode(HSB) | colorMode(HSB, 360, 100, 100, 100) |
colorMode(HSB, 360, 100, 100, 100) |
None |
| scale() mirror flip | scale(-1, 1); image(idle, -pos.x, ...) |
scale(-1, 1); image(idle, -pos.x, ...) |
None |
| PVector fields | vel.x, vel.y, pos.x, pos.y |
vel.x, vel.y, pos.x, pos.y |
None |
The breathing trick, movement math, braking, boundary checking, direction logic, and color mode are all word-for-word identical. The core algorithm survives the language crossing completely intact — only the input mechanism and one image-crop convention required non-trivial thought.
scripts/sergiosUpdatedSlimeJavaSketch.pde| Section | Lines | Content |
|---|---|---|
| File header | Top comment block | Port context, running instructions, full “what is different” summary including the image() convention warning |
| Constants | final float/int declarations |
CANVAS_SIZE, PLAYER_SPEED, PLAYER_SIZE, SPRITE_SCALE, ANIM_FRAME_MAX, ANIM_SPEED, VEL_THRESHOLD, key codes |
| Globals | Global declarations | PImage idle/back/bg, Player player, HashSet<Integer> heldKeys |
setup() |
Standard Processing setup | size(480,480), colorMode(HSB), loadImage(sketchPath(...)), new Player() |
draw() |
Painter’s layers | background clear → grass layer → WASD/arrow input → constrain → update → brake |
keyPressed() / keyReleased() |
Event handlers | Maintain heldKeys set — Java multi-key input pattern |
class Player |
Bottom of file | Constructor, drawPlayer() with corrected u2/v2 source params, update() |
I have loaded the pde into Processing Java. It’s not clear where I load the background images and the slime images for the code to function. Please advise.
The issue is that when Processing opens a .pde file, it looks for
images in a data/ subfolder inside the sketch folder —
it does not automatically resolve relative paths like ../images/. The
sketchPath() approach in the original code only works if Processing is
running from the original scripts/ location, which it often does not.
The reliable fix is two steps: add the images via the IDE (which puts them in the
right place automatically), then simplify the loadImage() calls to plain
filenames. Two options:
Option A — via the IDE (easiest):
Sketch → Add File… → navigate to each image and Open.
Processing copies it into the sketch’s data/ folder.
Repeat for all three images. The loadImage() calls now use plain filenames.
Option B — manually:
Sketch → Show Sketch Folder → create a data/ subfolder there
→ copy the three images in.
I used Option A and it worked perfectly. Let’s add these most recent accomplishments to our chatlog before we tackle the Pythonic version.
The loadImage() calls were updated from the sketchPath()
relative-path approach to plain filenames — the Processing standard:
// Before (sketchPath approach — unreliable when IDE relocates the sketch):
idle = loadImage(sketchPath("../images/slimeSprite.png"));
// After (plain filename — Processing always finds it in data/):
idle = loadImage("slimeSprite.png");
The file header was also updated to document Option A as the primary setup
method, with Option B (manual data/ folder) as the fallback.
sergiosUpdatedSlimePythonSketch.pyJust as we got the Java version working, let’s create the associated
Pythonic file: sergiosUpdatedSlimePythonSketch.py. We’ll test
it and then create the cross training file where we compare the 3 versions side
by side.
Session 7 predicted this would be “the most structural rethinking” of the Python
port. In the Java version, pos and vel are PVector objects
with .x and .y fields. In Python, the cleaner approach is
to replace them with four plain floats: pos_x, pos_y,
vel_x, vel_y.
| Operation | JavaScript (p5.js) | Java | Python |
|---|---|---|---|
| Create position | this.pos = createVector(x, y) |
pos = new PVector(x, y) |
self.pos_x = x; self.pos_y = y |
| Apply velocity | this.pos.add(this.vel) |
pos.add(vel) |
self.pos_x += self.vel_xself.pos_y += self.vel_y |
| Read position | this.pos.x |
pos.x |
self.pos_x |
| Acceleration hook | this.accel = createVector(0,0) |
accel = new PVector(0,0) |
self.accel_x = 0.0; self.accel_y = 0.0 |
The movement algorithm — add velocity to position each frame, zero velocity at the end — is completely unchanged. What changed is the container: PVector is absent, replaced by explicit components. This is the cross-training insight Session 7 identified: the concept travels; the library support is language-specific.
set() Replaces HashSet<Integer>Python’s built-in set() is the shortest path from the Java solution. The
behavior is identical; the syntax is simpler:
// Java:
HashSet<Integer> heldKeys = new HashSet<Integer>();
void keyPressed() { heldKeys.add(keyCode); }
void keyReleased() { heldKeys.remove(keyCode); }
if (heldKeys.contains(KEY_D)) { ... }
# Python:
held_keys = set()
def keyPressed(): held_keys.add(keyCode)
def keyReleased(): held_keys.discard(keyCode) # discard() is safe if absent
if KEY_D in held_keys: ...
One meaningful difference: discard() instead of remove().
Java’s HashSet.remove() silently succeeds whether or not the element exists.
Python’s set.remove() raises a KeyError if the element is absent;
set.discard() does not. In a sketch that can lose window focus mid-key-hold,
discard() is the safer choice — a dropped focus event means
keyReleased() never fires for the held key, but the next key-down/up
cycle should still work cleanly without an exception.
global Keyword — The One Structural SurprisePython requires an explicit global declaration when a function
reassigns a module-level name. Reading or calling methods on module-level
objects does not need global.
# Reassignment → needs global
def setup():
global idle, back, bg, player # we assign new objects to these names
idle = loadImage("slimeSprite.png")
player = Player(width / 2.0, height / 2.0)
# Attribute modification → no global needed
def draw():
player.vel_x += PLAYER_SPEED # modifying an attribute, not reassigning player
# Set method call → no global needed
def keyPressed():
held_keys.add(keyCode) # calling .add() on the existing set object
Java and JavaScript never needed this distinction because both use reference semantics with no scoping penalty: any variable declared in the outer scope (or at class level) is automatically accessible for both reading and reassignment inside a function, as long as it was declared before the function runs. Python enforces the separation explicitly.
image(): Same Corner Convention as JavaPython Processing is Jython calling directly into Java Processing’s compiled classes.
This means the 9-parameter image() uses the same convention as Java — top-left
corner + bottom-right corner, not p5.js’s offset + size:
# Python Processing (same as Java):
src_x = idle.height * frame_index
src_x2 = src_x + idle.height # bottom-right corner
image(idle, self.pos_x, self.pos_y, display_size, display_size,
src_x, 0, src_x2, idle.height)
The Python Processing official reference only documents the 4-parameter form of
image() because it is the most common. The 9-parameter overload is
available because Python Processing is calling Java Processing’s
PApplet.image() directly. The rule: if you are calling a Processing
function from Python Processing, look at the Java Processing reference for the
complete signature, not just the Python reference.
| Feature | Java | Python | Change |
|---|---|---|---|
| Velocity clamping | constrain(vel.x, -SPEED, SPEED) |
constrain(vel_x, -PLAYER_SPEED, PLAYER_SPEED) |
Function name identical |
| Braking | player.vel.x = 0; player.vel.y = 0; |
player.vel_x = 0; player.vel_y = 0 |
Attribute name only |
| Breathing trick | frame += ANIM_SPEED; (int)frame |
self.frame += ANIM_SPEED; int(self.frame) |
Cast syntax only |
| Boundary checking | player.pos.x <= width - size/2 |
player.pos_x <= width - player.size / 2 |
Attribute name only |
| Direction logic | if (vel.y < 0 && vel.x == 0) |
if self.vel_y < 0 and self.vel_x == 0: |
Syntax only |
| Mirror flip | pushMatrix(); scale(-1,1); ... popMatrix() |
pushMatrix(); scale(-1,1); ... popMatrix() |
None |
| colorMode(HSB) | colorMode(HSB, 360, 100, 100, 100) |
colorMode(HSB, 360, 100, 100, 100) |
None |
| imageMode(CENTER) | imageMode(CENTER) |
imageMode(CENTER) |
None |
| Image loading / data/ folder | loadImage("slimeSprite.png") |
loadImage("slimeSprite.png") |
None |
scripts/sergiosUpdatedSlimePythonSketch.py| Section | Python feature | Notes |
|---|---|---|
| Constants | UPPER_CASE module-level names | Same values as Java/JS; no final — convention only |
| Globals | None declarations + set() |
Mirroring Java’s uninitialized global declarations |
setup() |
global idle, back, bg, player |
Required for reassignment; loadImage() synchronous |
draw() |
No global needed |
Attribute writes don’t reassign the module-level name |
keyPressed() / keyReleased() |
held_keys.add() / .discard() |
discard() chosen over remove() for focus-loss safety |
class Player |
__init__, self, snake_case methods |
pos_x/pos_y/vel_x/vel_y replace PVector; algorithm unchanged |
Setup instructions: install Python Mode via Tools → Manage Modes, switch the IDE mode selector to Python, then use Sketch → Add File… three times for the images — same pattern as the Java version.
sergiosUpdatedSlimeCTraining.htmlEach of our slime apps work correctly in their respective venues. We’d like
to make a file, similar to our twinkling stars cross training file, that shows the
3 applications side by side. Can you make sergiosUpdatedSlimeCTraining.html
with associated files sergiosUpdatedCTrainingScripts.js (partially
complete already?) and sergiosUpdatedCTrainingStyles.css.
fetch() Instead of Inline String.rawThe twinkling stars cross-training page embeds all three code strings as
String.raw template literals directly in the HTML. The slime files are
substantially longer (~250–300 lines each), so embedding all three inline would produce
an unwieldy HTML file. The slime showcase page already uses fetch() successfully,
so the same pattern was applied here:
// Global vars — populated by fetch(), read by copyRaw()
var JS_CODE = '', JAVA_CODE = '', PY_CODE = '';
fetch('scripts/sergiosUpdatedSlimeSketch.js')
.then(function(r) { return r.text(); })
.then(function(code) {
JS_CODE = code; // stored for copyRaw()
buildCodeDisplay(code, 'jsCodeList', highlightJS);
});
Each copy button calls copyRaw(this, JS_CODE) — reading from the
stored raw variable, never from DOM textContent. This follows the rule in
the TNT code-display note: DOM reading strips HTML entities inconsistently across browsers.
| File | Purpose |
|---|---|
sergiosUpdatedSlimeCTraining.html |
Main cross-training page. Hero with grass background, 6 concept cards, 3 code panels. |
styles/sergiosUpdatedCTrainingStyles.css |
Slime-green theme. Three color-keyed code panels: yellow (JS), blue (Java), green (Python). |
scripts/sergiosUpdatedSlimeCTrainingScripts.js |
Code display + copy utilities. Completed in an earlier session; includes copyRaw() and the applyKW HTML-tag safety check. |
For convenience, should the JavaScript code to be copied include the Player class source code? The other code snippets include it and it makes testing it in the p5js editor a bit easier, unless we include a copy button for the Player code as well. What’s the best approach?
The cleanest approach is to display the combined sketch.js + Player.js in the JS panel — matching what Java and Python already show. Three reasons:
- All three panels then display a complete, runnable single-file sketch, making the comparison symmetrical.
- The copy button works in one click: paste into the p5.js editor and run, no separate merging step.
- Students can scroll down in the JS panel to see the
Playerclass JavaScript syntax alongside Java and Python — concept card #5 (“The Player Class — Same Concept, Three Syntaxes”) requires this to be visible in all three panels.
The two-file teaching point is preserved in the panel label (“sketch.js + Player.js (combined)”) and in the panel footer note, and the individual download buttons still offer both files separately.
Promise.all() to Fetch and ConcatenateBoth files are fetched in parallel and joined with a visible separator comment before
being stored in JS_CODE and passed to buildCodeDisplay():
Promise.all([
fetch('scripts/sergiosUpdatedSlimeSketch.js').then(function(r) { return r.text(); }),
fetch('scripts/Player.js').then(function(r) { return r.text(); })
]).then(function(parts) {
JS_CODE = parts[0]
+ '\n\n// ─────────────────────────────────────────────────────────\n'
+ '// Player.js (companion file — combined here for editor use)\n'
+ '// ─────────────────────────────────────────────────────────\n\n'
+ parts[1];
buildCodeDisplay(JS_CODE, 'jsCodeList', highlightJS);
});
The separator comment makes the join point obvious to anyone reading the panel.
copyRaw(this, JS_CODE) copies the full combined string —
paste directly into the p5.js editor, upload the three images, and run.
Excellent, the JS copied code works perfectly in the p5js editor. Let’s update our chatlog to include this discussion. I’ll be testing the other files momentarily.
← CharacterThe original pde file for Java works, but when I copy the code from the Java section in the cross training page, I get this error: “Maybe there’s an unclosed paren or quote mark somewhere before this line?” I don’t see the precise area of the error. The Python code worked perfectly by the way. Incidentally, the html page validated for what that is worth.
The error is a Processing preprocessor tokenization failure. The original file works because Processing reads UTF-8 from disk, handling all Unicode characters correctly. The pasted version fails because Windows clipboard text is transmitted as UTF-16 and Processing’s IDE may handle certain Unicode code points differently when receiving pasted text vs. reading a file.
Comparing the Java file against the Python file (which worked perfectly), there is
exactly one Unicode character unique to the Java file: ←
(U+2190, LEFTWARDS ARROW). It appears twice in the block comment header, describing the
9-parameter image() source convention:
* p5.js: image(img, ..., sx, sy, sw, sh) ← source: offset + SIZE // problematic * Java: image(img, ..., u1, v1, u2, v2) ← source: TOP-LEFT + BOTTOM-RIGHT
The Python file uses → (U+2192) extensively and works fine —
so rightward arrows are not the issue. The ← character is the only
difference. While Processing should ignore everything inside a /* */ block
comment, there appears to be a clipboard-specific code path in the Processing IDE on
Windows where U+2190 disrupts the tokenizer state, even inside a comment block.
Fix: replace both occurrences of ← with the
ASCII two-character sequence <-:
* p5.js: image(img, ..., sx, sy, sw, sh) <- source: offset + SIZE * Java: image(img, ..., u1, v1, u2, v2) <- source: TOP-LEFT + BOTTOM-RIGHT CORNER
Ok, that is working now! Let’s do a final ‘sanity check’ of the cross training file and make sure it’s tied to our original p5js showcase file. Let’s update our chatlog accordingly and then wire the app up to our cross training home page.
| File | Change |
|---|---|
sergiosUpdatedSlime.html |
Spark bar: added App Comparison link to sergiosUpdatedSlimeCTraining.html.Cross-training card: updated title to “JS, Java & Python — Side by Side” and added prominent comparison link. Page nav: added App Comparison nav button. |
creativeCrossTrainingHome.html |
Added Entry #3 — Sergio’s Slime Sprite card. Links: showcase, app comparison, original sketch, chat log. Priority queue renumbered: Henry’s Ghost full rebuild becomes #4, student sketches become #5+. lastUpdate updated to 10/01/2026. |
Sergio’s Slime Sprite: