Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 42 additions & 5 deletions packages/odyc/src/canvas.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,57 @@
import { createSingleton } from './lib'

type CanvasParams = { id: string; zIndex?: number }
type CanvasParams = {
id: string
zIndex?: number
root?: HTMLElement | string | null
}

export class Canvas {
element: HTMLCanvasElement

constructor({ id, zIndex }: CanvasParams) {
#isFullscreen = true

constructor({ id, zIndex, root = null }: CanvasParams) {
this.element = document.createElement('canvas')

// If developer doesnt provide custom root, put it to body and assume it's the only thing on page
if (root) {
this.#isFullscreen = false
} else {
root = document.body
}

this.element.addEventListener('keydown', (e) => {
if (
document.activeElement &&
document.activeElement.classList.contains('odyc-canvas')
) {
e.preventDefault()
} else {
alert('Problem')
}
})

this.element.classList.add('odyc-canvas')
this.element.setAttribute('tabindex', '0')
this.element.style.setProperty('display', 'block')
this.element.style.setProperty('position', 'absolute')
this.element.style.setProperty('image-rendering', 'pixelated')
if (id) this.element.classList.add(id)
if (zIndex) this.element.style.setProperty('z-index', zIndex.toString())
document.body.append(this.element)

const element =
typeof root === 'string' ? document.querySelector(root) : root
element?.appendChild(this.element)
window.addEventListener('resize', this.#fitToScreen)
}

show() {
this.element.style.setProperty('display', 'block')
this.element.style.setProperty('opacity', '1')
}

hide() {
this.element.style.setProperty('display', 'none')
this.element.style.setProperty('opacity', '0')
}

setSize(width: number, height: number) {
Expand All @@ -44,6 +75,12 @@ export class Canvas {
}

#fitToScreen = () => {
if (!this.#isFullscreen) {
this.element.style.height = '100%'
this.element.style.outline = 'none'
return
}

const orientation =
this.element.width < this.element.height ? 'vertical' : 'horizontal'
const sideSize = Math.min(window.innerWidth, window.innerHeight)
Expand Down
2 changes: 2 additions & 0 deletions packages/odyc/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export type Config<T extends string> = RendererParams &
DialogParams & { filter?: FilterParams } & GameStateParams<T> & {
/** Game title displayed at the start of the game */
title?: string | string[]
} & {
root?: HTMLElement | string
}

export const defaultConfig: Config<string> = {
Expand Down
5 changes: 4 additions & 1 deletion packages/odyc/src/createGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ export const createGame = <T extends string>(
const dialog = initDialog(config)
const prompt = initPrompt(config)
const messageBox = initMessageBox(config)
const gameFilter = initFilter(renderer.canvas.element, config.filter)
const gameFilter = initFilter(renderer.canvas.element, {
...config.filter,
root: config.root,
})
const ender = initEnder({ gameState, messageBox, camera })

const renderGame = debounce(() => {
Expand Down
7 changes: 6 additions & 1 deletion packages/odyc/src/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type DialogParams = {
/** Dialog typing speed ('SLOW', 'NORMAL', 'FAST') */
dialogSpeed: keyof typeof DIALOG_SPEED
colors: RendererParams['colors']
root?: HTMLElement | string
}

export class Dialog {
Expand Down Expand Up @@ -68,7 +69,11 @@ export class Dialog {
this.#borderColor = resolveColor(params.dialogBorder, params.colors)
this.#charactersIntervalMs = DIALOG_SPEED[params.dialogSpeed]

this.#canvas = getCanvas({ id: DIALOG_CANVAS_ID, zIndex: 10 })
this.#canvas = getCanvas({
id: DIALOG_CANVAS_ID,
zIndex: 10,
root: params.root,
})
this.#canvas.setSize(DIALOG_CANVAS_SIZE, DIALOG_CANVAS_SIZE)
this.#canvas.hide()
this.#ctx = this.#canvas.get2dCtx()
Expand Down
2 changes: 1 addition & 1 deletion packages/odyc/src/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class Filter {
}

this.#textureSource = target
this.canvas = getCanvas({ id: FILTER_CANVAS_ID })
this.canvas = getCanvas({ id: FILTER_CANVAS_ID, root: options.root })

const gl = this.canvas.getWebglCtx()
if (!gl) throw new Error('WebGL not supported')
Expand Down
22 changes: 22 additions & 0 deletions packages/odyc/src/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type Input = 'LEFT' | 'UP' | 'RIGHT' | 'DOWN' | 'ACTION'
export type InputsHandlerParams = {
/** Key bindings for each input action - maps input types to keyboard event codes */
controls: Record<Input, string | string[]>
root?: HTMLElement | string
}

class InputsHandler {
Expand All @@ -24,6 +25,8 @@ class InputsHandler {
oldTouchY?: number
pointerId?: number
isSliding = false
#touchEventElement: HTMLElement
#isFullscreen = true

static get touchEventElement() {
return
Expand All @@ -49,6 +52,8 @@ class InputsHandler {
touchEventElement.addEventListener('pointerleave', this.handleTouchLeave)
touchEventElement.addEventListener('pointercancel', this.handleTouchLeave)
touchEventElement.addEventListener('pointermove', this.handleTouchMove)

this.#touchEventElement = touchEventElement
}

init(params: InputsHandlerParams, onInput: (input: Input) => void) {
Expand All @@ -63,6 +68,11 @@ class InputsHandler {
string | string[],
][]
this.onInput = onInput
this.#isFullscreen = params.root === undefined

if (!this.#isFullscreen) {
this.#touchEventElement.style.display = 'none'
}
}

handleTouch = (e: PointerEvent) => {
Expand Down Expand Up @@ -122,6 +132,18 @@ class InputsHandler {
}

handleKeydown = (e: KeyboardEvent) => {
// If not fullscreen, only handle events made inside canvas
if (!this.#isFullscreen) {
if (
document.activeElement &&
document.activeElement.classList.contains('odyc-canvas')
) {
e.preventDefault()
} else {
return
}
}

const entrie = this.controls.find(
([_, keys]) => keys === e.code || keys.includes(e.code),
)
Expand Down
7 changes: 6 additions & 1 deletion packages/odyc/src/messageBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type MessageBoxParams = {
/** Text color for message content (color index or CSS color) */
messageColor: string | number
colors: RendererParams['colors']
root?: HTMLElement | string
}

export class MessageBox {
Expand Down Expand Up @@ -53,7 +54,11 @@ export class MessageBox {
this.#canvasSize / (8 + this.#spaceBetweenLines),
)

this.#canvas = getCanvas({ id: MESSAGE_CANVAS_ID, zIndex: 10 })
this.#canvas = getCanvas({
id: MESSAGE_CANVAS_ID,
zIndex: 10,
root: params.root,
})
this.#canvas.hide()
this.#ctx = this.#canvas.get2dCtx()
this.#canvas.setSize(this.#canvasSize, this.#canvasSize)
Expand Down
6 changes: 5 additions & 1 deletion packages/odyc/src/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ export class Prompt {
this.#contentColor = this.#getColor(params.dialogColor)
this.#borderColor = this.#getColor(params.dialogBorder)

this.#canvas = getCanvas({ id: PROMPT_CANVAS_ID, zIndex: 10 })
this.#canvas = getCanvas({
id: PROMPT_CANVAS_ID,
zIndex: 10,
root: params.root,
})
this.#canvas.setSize(PROMPT_CANVAS_SIZE, PROMPT_CANVAS_SIZE)
this.#canvas.hide()
this.#ctx = this.#canvas.get2dCtx()
Expand Down
3 changes: 2 additions & 1 deletion packages/odyc/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type RendererParams = {
colors: string[]
/** Background color (color index or CSS color) */
background?: string | number
root?: HTMLElement | string
}

class Renderer {
Expand All @@ -52,7 +53,7 @@ class Renderer {
this.colors = options.colors
this.background = options.background

this.canvas = getCanvas({ id: RENDERER_CANVAS_ID })
this.canvas = getCanvas({ id: RENDERER_CANVAS_ID, root: options.root })
this.canvas.show()

this.ctx = this.canvas.get2dCtx()
Expand Down
5 changes: 4 additions & 1 deletion packages/odyc/src/shaders/filterSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,14 @@ type CustomFilterSettings = {
* }
* ```
*/
export type FilterParams =
export type FilterParams = (
| {
[K in FilterKey]: FilterSettingsOf<K>
}[FilterKey]
| CustomFilterSettings
) & {
root?: HTMLElement | string
}

export const getFilterSettings = (settings: FilterParams) => {
if ('name' in settings) {
Expand Down
29 changes: 29 additions & 0 deletions tests/odyc-e2e/functional/game-root-element/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { expect, test } from 'vitest'
import { init } from './index'
import { page } from '@vitest/browser/context'

test('game canvases render inside an element', async () => {
const div = document.createElement('div')
div.id = 'game-root'
document.body.appendChild(div)

const { game, state } = init()

const innerEl = document.querySelector('#game-root') as HTMLElement
if (!innerEl) {
throw new Error('Game root element not found.')
}

const rendererCanvasEl = document.querySelector(
'#game-root .odyc-renderer-canvas',
) as HTMLElement
if (!rendererCanvasEl) {
throw new Error('Game canvas element not found.')
}

const bodyLocator = page.elementLocator(document.body)
const innerLocator = page.elementLocator(innerEl)

await expect.element(bodyLocator).toContainElement(innerEl)
await expect.element(innerLocator).toContainElement(rendererCanvasEl)
})
24 changes: 24 additions & 0 deletions tests/odyc-e2e/functional/game-root-element/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createGame } from 'odyc'

const state = {}

export const init = () => {
const el = document.getElementById('game-root')

if (!el) {
throw new Error('Game root HTML element missing')
}

const game = createGame({
root: el,
map: `.`,
screenWidth: 1,
screenHeight: 1,
player: {
sprite: 3,
position: [0, 0],
},
})

return { game, state }
}
29 changes: 29 additions & 0 deletions tests/odyc-e2e/functional/game-root-selector/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { expect, test } from 'vitest'
import { init } from './index'
import { page } from '@vitest/browser/context'

test('game canvases render inside an element', async () => {
const div = document.createElement('div')
div.id = 'game-root'
document.body.appendChild(div)

const { game, state } = init()

const innerEl = document.querySelector('#game-root') as HTMLElement
if (!innerEl) {
throw new Error('Game root element not found.')
}

const rendererCanvasEl = document.querySelector(
'#game-root .odyc-renderer-canvas',
) as HTMLElement
if (!rendererCanvasEl) {
throw new Error('Game canvas element not found.')
}

const bodyLocator = page.elementLocator(document.body)
const innerLocator = page.elementLocator(innerEl)

await expect.element(bodyLocator).toContainElement(innerEl)
await expect.element(innerLocator).toContainElement(rendererCanvasEl)
})
18 changes: 18 additions & 0 deletions tests/odyc-e2e/functional/game-root-selector/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createGame } from 'odyc'

const state = {}

export const init = () => {
const game = createGame({
root: '#game-root',
map: `.`,
screenWidth: 1,
screenHeight: 1,
player: {
sprite: 3,
position: [0, 0],
},
})

return { game, state }
}