Skip to content

10 Note Drawing

In this chapter, we will implementing drawing logic of Note.

Visual Time

Let's calculate a note's visual time, which consists of minimum and maximum visual times, and refactor spawn time to use the minimum visual time:

TypeScript
export class Note extends Archetype {
    // ...

    visualTime = this.entityMemory(Range)

    // ...

    preprocess() {
        // ...

        this.visualTime.copyFrom(Range.l.add(this.targetTime))

        this.spawnTime = this.visualTime.min
    }

    // ...
}
JavaScript
export class Note extends Archetype {
    // ...

    visualTime = this.entityMemory(Range)

    // ...

    preprocess() {
        // ...

        this.visualTime.copyFrom(Range.l.add(this.targetTime))

        this.spawnTime = this.visualTime.min
    }

    // ...
}

Declaring

Just like Stage, we use the standard sprite for our note for simplicity:

TypeScript
export const skin = defineSkin({
    sprites: {
        // ...
        note: SkinSpriteName.NoteHeadCyan,
    },
})
JavaScript
export const skin = defineSkin({
    sprites: {
        // ...
        note: SkinSpriteName.NoteHeadCyan,
    },
})

Drawing

With both minimum and maximum visual time, as well as the current time, we can calculate note's y position. Because we transformed screen coordinate system to go from y = 0 (top of note spawn) to y = 1 (judgment line), this greatly simplifies our calculation:

TypeScript
export class Note extends Archetype {
    // ...

    updateParallel() {
        const y = Math.unlerp(this.visualTime.min, this.visualTime.max, time.now)
    }
}
JavaScript
export class Note extends Archetype {
    // ...

    updateParallel() {
        const y = Math.unlerp(this.visualTime.min, this.visualTime.max, time.now)
    }
}

With y position calculated, we can draw the note:

TypeScript
export class Note extends Archetype {
    // ...

    updateParallel() {
        // ...

        const layout = Rect.one.mul(note.radius).scale(1, -1).translate(0, y)

        skin.sprites.note.draw(layout, [1], 1)
    }
}
JavaScript
export class Note extends Archetype {
    // ...

    updateParallel() {
        // ...

        const layout = Rect.one.mul(note.radius).scale(1, -1).translate(0, y)

        skin.sprites.note.draw(layout, [1], 1)
    }
}

Z Fighting

While this is working already, there is a hidden issue we have not solved yet: z fighting.

It is referred to when multiple objects are rendered with the same z value, their ordering may not be consistent from frame to frame and can be flickering if they overlap.

To solve this, let's set the secondary z order to be negative target time, so that earlier notes will always be on top of later notes.

TypeScript
export class Note extends Archetype {
    // ...

    updateParallel() {
        // ...

        skin.sprites.note.draw(layout, [1, -this.targetTime], 1)
    }
}
JavaScript
export class Note extends Archetype {
    // ...

    updateParallel() {
        // ...

        skin.sprites.note.draw(layout, [1, -this.targetTime], 1)
    }
}