Summary#
A texture is a file in your app, not data. Put a .png in model/textures/ and it becomes part of your app's
vocabulary:
model/
textures/
wall.png
floor.png
sprites.pngwall.png is now drawable as Draw.Image(wall, …). There is nothing to register and nothing to import.
Textures are the third kind of art an app can ship, and they divide by what the picture is rather than by taste:
| you have | put it in | draw it with |
|---|---|---|
| a single-colour glyph that should follow your text | model/icons/ | Icon(Icons.Search) |
| a vector illustration, logo or background | model/art/ | Svg(Art.Hexgrid) |
| a bitmap — a wall texture, a sprite sheet, a photograph | model/textures/ | Draw.Image(wall, …) |
Signature#
Draw.Image(wall, dx, dy, dw, dh) // the whole texture, into a destination rectangle
Draw.Image(wall, sx, sy, sw, sh, dx, dy, dw, dh) // a SOURCE rectangle of it, into a destination one
Draw.Image(url, …) // the same two forms, over a runtime urlDescription#
The name is checked#
Draw.Image(wall, …) names the texture by a bare identifier, checked against the textures your app actually
ships. A typo is a compile error that lists what there is:
`Draw.Image` takes a declared texture or a url — 'walll' is neither a variable in scope nor a
declared texture (declared: floor, wall). Did you mean 'wall'?Because the name is an identifier, a texture's file name must be one too — brick_wall.png, not
brick-wall.png. A kebab-case file is rejected with the rename to make.
A url still works, and a collision is refused#
Unlike Icon and Svg, this first argument can legitimately be an expression: a url is genuinely a runtime
value sometimes — a user's uploaded avatar, a signed download link.
[Page("/avatar")]
[AllowAnonymous]
component Avatar() {
string src = "/uploads/me.png";
on frame (double dt) {
Draw.Clear("#111");
Draw.Image(src, 8, 8, 64, 64); // an expression — the url form
}
render { Canvas(w: 80, h: 80); }
}The texture form is the same call with a declared name in that first position — and both argument forms in one page, which is the whole surface:
[Page("/wall")]
[AllowAnonymous]
component Wall() {
on frame (double dt) {
Draw.Clear("#1a1a22");
Draw.Image(Textures.Brick1, 0, 0, 32, 32); // the WHOLE texture, into a destination rectangle
Draw.Image(Textures.Brick1, 0, 0, 4, 4, 40, 0, 64, 64); // a SOURCE rectangle of it, into a destination one
}
render { Canvas(w: 112, h: 64); }
}That example is compiled on every docs build against a real image file — canvas-texture is a complete sample
app shipping textures/wall.png, and osy docs sample canvas-texture hands you the whole thing.
So a bare identifier could mean either, and when it means both the compiler refuses rather than picking:
`Draw.Image(wall, …)` is ambiguous — 'wall' is both a declared texture and a variable in scope,
and the two draw different things. Rename one of them.Every precedence rule here would produce a silent bug in one direction or the other — a texture drawn where a variable was meant, or a local added months later quietly changing what a call site draws. Renaming one of the two costs seconds; finding either of those costs an afternoon.
Which formats, and why the file name does not decide#
.png, .jpg/.jpeg and .webp — the three raster formats a browser decodes into a canvas.
The type a texture is served under is read from its bytes, never from its extension. A consequence worth knowing: a file whose name disagrees with its contents is a compile error rather than a quiet re-label.
'wall.png' is really a JPEG, whatever its extension says — rename it to 'wall.jpg'.An .svg in model/textures/ is not a texture. A vector image is markup that has to be sanitized before it
reaches a page, which is what model/art/ and Svg(name) are for.
Sampling a source rectangle#
The nine-argument form takes a rectangle of the texture and scales it into a rectangle of the canvas. It is what a sprite sheet needs, and what a textured raycaster needs — a one-pixel-wide column of the texture stretched to a wall's height:
// one screen column: texel column `texX` of a 64x64 texture, over the wall's full height
Draw.Image(wall, texX, 0, 1, 64, x, top, colWidth, wallHeight);Shading a texture#
A blit paints the texture's own pixels, so lighting is a second pass over the top rather than a colour argument — draw the texture, then wash it with a translucent rectangle:
Draw.Image(wall, texX, 0, 1, 64, x, top, colWidth, wallHeight);
Draw.Rect(x, top, colWidth, wallHeight, "rgba(0,0,0,0.35)"); // distance falloffOne texture per name#
Two files claiming the same stem is an error naming both, because a name has to resolve to one file. The same rule applies when a kit vendors its textures into your tree.
Where texture files live, and how to move them#
The default is any textures/ folder in your source tree. Declare the role in app.osy to put them somewhere
else:
app Arcade {
model "model/**/*.osy";
textures "assets/textures/*.png";
}Reading a texture's pixels#
Texture.Pixels(wall) answers the texture's pixels as a List<int> — one packed 0xRRGGBB colour per pixel, in
row order — so a software renderer can sample it. Texture.Width(wall) and Texture.Height(wall) give its size.
It answers the whole buffer, once, rather than a texel at a time, and that is a performance contract rather than a convenience: a call costs roughly seven times an arithmetic operation, so asking per texel would cap a per-pixel effect at a few thousand pixels a frame before any of its own work. Read it into a field, index it in the loop.
⚠ A texture is decoded by the browser, so
Texture.Pixelsanswers an empty list until it has been — the same "one frame away" ruleDraw.Imagefollows. Read it in the frame body until it arrives, not inon mount, which runs once and would lose the race permanently.
List<int> texels = new List<int>();
on frame (double dt) {
if (texels.Count == 0) { texels = Texture.Pixels(wall); }
// …now index it: texels[Texture.Width(wall) * y + x]
}Colours are 24-bit RGB, not 32-bit ARGB, because an Osy# int is a signed 32-bit integer — any alpha above 0x7F
would overflow it and hand you negative colours. A pixel buffer is opaque; transparency is expressed by not
drawing.
Size, and the limits#
A texture's intrinsic size is read at compile time, from the image header — so it is known before anything decodes, and a header claiming an absurd size is refused there rather than becoming a very large canvas later. A texture may be at most 8192×8192 and 8 MB.
Passing a texture around — Textures is a type#
Textures is a type, so a texture is a value you can pass and store like any other. A helper that blits one takes
it as a parameter:
void Blit(Textures tex, int x) {
Draw.Image(tex, x, 0, 128, 128);
}
// at the call site
Blit(Textures.Wall1, 0);The same holds for a return type, a local, and a field on an entity. Because a stored texture is keyed by the file's name rather than by a position in a list, adding a new file never changes what an already-stored row means.