Skip to content

05 Note Drawing

In this chapter, we will map each note's time to a panel position and draw the note with undistorted proportions.

Existing Sprite

The shared Skin class in guide/lib/skin.py already maps note to a standard cyan note sprite from Play. Preview reuses this field, so confirm it is present but do not add another declaration:

python
@skin
class Skin:
    # ...

    note: StandardSprite.NOTE_HEAD_CYAN

Scaled Screen

Unlike play mode, preview uses different scales for the x and y axes. One local x unit spans screen().h / 20, while one local y unit spans screen().h / PANEL_HEIGHT. A local height must therefore be multiplied by the ratio of these scales, PANEL_HEIGHT / 20, to have the same size on screen as a local width.

To draw notes with the correct proportions, add a height scale factor to guide/preview/chart.py:

python
HEIGHT_SCALE = PANEL_HEIGHT / 20

Calculating Position

In guide/preview/chart.py, add helpers that map a time to a position. Floor division selects the panel and the remainder gives the vertical position within it:

python
from sonolus.script.vec import Vec2


def x_at_time(time: float):
    return (time // PANEL_HEIGHT) * PANEL_WIDTH


def y_at_time(time: float):
    return time % PANEL_HEIGHT


def pos_at_time(time: float):
    return Vec2(x_at_time(time), y_at_time(time))

Drawing

In guide/preview/note.py, add the Rect, Vec2, Options, and Skin imports shown below. Replace the existing guide.preview.chart import with the combined import shown here, then add render. The height scale keeps the note square on screen despite the different local x and y scales:

python
from sonolus.script.quad import Rect
from sonolus.script.vec import Vec2

from guide.lib.options import Options
from guide.lib.skin import Skin
from guide.preview.chart import HEIGHT_SCALE, Chart, pos_at_time


class PreviewNote(PreviewArchetype):
    # ...

    def render(self):
        time = beat_to_time(self.beat)
        position = pos_at_time(time)
        layout = Rect.from_center(
            position,
            Vec2(2 * Options.note_size, 2 * Options.note_size * HEIGHT_SCALE),
        )

        Skin.note.draw(layout, z=(2, -time))

Layer 2 keeps Notes above the stage and the timing lines added later. As in Play chapter 10, the second value in the z tuple keeps overlapping Notes in a stable order.

Checkpoint

Refresh Preview. Every sample note should appear in the panel that contains its target time, and each note should keep the same proportions it has in Play.