Ynet.dev

This is a framework for HTML 5 network apps; games and tools in particular.

Ynet internally handles dynamic loading of code modules (aka. JavaScript streaming), which also allows live code updates (aka. runtime code injection), speculative module fetching and seamless resuming after re-connection (without reloading any code, aka. lazy execution). Since it acts as a pre-compiler, some syntactic sugar is also included (see below).

Latest release: 1.24.3 (major.y.m)

Capabilities

  • built-in debug console, supporting colored output
  • meaningful typeOf( var ) where null is "null" and "Ynet" is "string"
  • compact HTML tree notation
  • custom/complex HTML elements (built-in switch, ring bar, password with revealer)
  • query and manipulate elements, classes, attributes, etc.
  • easy interface events (supporting click/tap/hold/drag & drop)
  • built-in authentication (Passkey / FIDO2, device / cookie, password, e-mail, QR code)
  • session handler (database or memory)
  • resource loader / progress display
  • handy geometry functions, curves/slopes/splines, vectors and matrix classes
  • n-dimensional typed arrays, unlimited 2D/3D maps, bsp/quad/oct trees, heaps
  • keyboard, mouse, touch and gamepad support
  • database access, including query helpers
  • heartbeat providing a stable "main loop" framerate, also fps measuring
  • tile maps, including A* path finding, terrain generation and more
  • voxel trees for endless worlds, supporting voxel tile maps for quick editing
  • 2D/Canvas rendering and drawing utilities, including image effects
  • 3D/WebGL rendering with shaders, covering pixels / sprites / polygons / voxels
  • sound and music playback, supporting multiple effects (pitch, reverb, distort, etc.)
  • sound sprites (multiple sounds in one file)
  • QR data transfer (ie. reader and generator)

Screenshots

Here are some random screenshots from various projects:

JS syntactic sugar

The delay( ms ) structure is a shortcut for setTimeout() calls:

delay( 1000 ) { doSomethingAfterOneSecond(); }


The measure( name ) structure stores the execution time of a certain code piece:

measure( "render" ) { doRenderingHere(); }

The duration will be available in core.stats.render.


The benchmark( num ) structure also measures time, but for multiple iterations:

benchmark( 1000 ) { doSomethingThousandTimes(); }

After 1000 iterations, the execution duration in ms will be printed to the Ynet console.


Easily translate values to strings:

var list = { dog:"bark", cat:"meow", bird:"chirp" }; var fallback = "something"; print( "Cat says " + select( "cat", list, fallback ) ); print( "Fish says " + select( "fish", list, fallback ) );

Prints Cat says meow and Fish says something.


Ynet supports multi-line strings in code:

var list = "/* This is a multi-line string. */";


Easily check arguments for their data type and range:

function printText( id, name, message, opacity ) { /// id = uint /// name = char (16) [a-z] /// message = text (2k) /// opacity = float (0-100) }

In this example id can be any positive integer, name must be a string of exactly 16 lowercase letters, message is a UTF8 string up to 2000 characters length and opacity can be any number from 0 to 100. Failing a test throws an error, providing name, data type, value and expected range of the argument.


Quickly print out debug information:

function start() { //> Now starting }

The text Now starting will be printed to the console.


Error handling and tracing:

tryce( "myEventName", 20 ) { foo = 1; }

Prints: myEventName, 20, ReferenceError: foo is not defined, and a stack trace.


Simplest counting loops:

for( var a to 5 ) print( a );

Prints: 0 1 2 3 4

for( var a from 5 ) print( a );

Prints: 4 3 2 1 0


Simple counting loops:

for( var a = 2 to 5 ) print( a );

Prints: 2 3 4

for( var a = 2 from 5 ) print( a );

Prints: 4 3 2


Stepped counting loops:

for( var a = 0 to 5 step 2 ) print( a );

Prints: 0 2 4

for( var a = 0 from 5 step 2 ) print( a );

Prints: 4 2 0


Iterating through array values:

var list = [ "The", "answer", "is", "42" ]; for( list as value ) { print( value ); }

Prints: The answer is 42


Iterating through array indices and values / also works on Map and map-like objects:

var list = [ "Ynet", ".", "dev" ]; for( list as index => value ) { print( index + " = " + value ); }

Prints: 0 = Ynet 1 = . 2 = dev


Alias syntax for often-used functions:

$client.getTile( x,y ) { // The actual function } $client.get => getTile; // Function alias

The getTile function can now be called through item.get( 0,0 );

HTML elements

Easily create HTML interfaces:

var content = html ( "/* <main.homescreen> <h1> Welcome <p#intro> This is a sample application. <p> Here's a list of cars: <ul.items> <li.grey> DeLorean DMC-12 <li.black> Batmobile <li.blue> Mach 5 <p.form> <input/text #txtName hint="Enter name" assign="onNameChange"> <input/int #txtNumber.right hint="Enter number" value="2"> <select id="lstCountry" hint="Select country"> <option content="Lothlorien"> <option content="Oceania"> <option content="Gilead"> <button assign="onButtonClick" content="Continue"> */" );

There are multiple ways to define the id, class and content (eg. p#intro and p id="intro" is equivalent). Some special attributes (such as assign) will be recognized and assigned to the corresponding Ynet UI events. The final append() call adds this structure to the body.


SVG elements can be accessed through their respective namespace:

html( "<svg:line>" );


Furthermore $ allows using custom complex HTML elements.

html( "<$switch>" ).text( "Enable dark theme" );

This creates your everyday "on/off" switch with a sliding bullet.


Radio button groups can be assembled as follows:

html( "<options>" ) .assign( "selectAction" ) .addOption( "fruit", "Eat fruit" ) .addOption( "water", "Drink water" ) .addOption( "table", "Dance on table" );

Interacting with the radio buttons will trigger the selectAction event to handle this.value().


Run a CSS animation and hide the element afterwards:

myButton.ani( "myAnimation", "hide" );

Internally a timer will wait for the animation to finish and then execute the specified action or function.

CSS syntactic sugar

body { color:#000; } // Inline comment

CSS only allows /* block comments */ but Ynet fixes this stupid decision.

div { scroll:y; }

Allow scrolling in x, y or xy direction. Translates to a overflow and touch-action setup.

div { display:grid; cols:4 10em; }

Display as table grid with 4 columns (or less), retaining a minimum width of 10em per column.

div { display:grid; cols:5em 20% 2px; }

Display as table grid with a column size between 5em and 20%, seperated by a 2px gap.

border-left-right:1px solid;

border-top-bottom:1px solid;

Border setup for two opposite sides.

if( w < 200 )

if( h >= 10em )

Included if the screen width / height matches the condition.

if( ws )

if( hs )

Included if the screen is in landscape / portrait mode.

if( cursor )

if( touch )

Included if the primary input is the mouse cursor / touch screen.

button:hover { background:#FFF; }

Ynet automatically applies :hover styles only for mouse cursors.

button:ui { background:#FFF; }

The custom :ui selector applies to both, :focus and :hover. This allows easily setting up the same styles for mouse and keyboard interaction.

$red = #F00; body { color:$red; }

CSS variables made simple.

Conditional CSS

To avoid the limited CSS media queries and their weird syntax, Ynet offers custom CSS conditions:

$css( w < 200 || aspect < 1.0 || core.domain.indexOf( "m." ) == 0 ) { nav, main, footer { display:block; width:100%; } }

This style will be included on small windows w < 200, in portrait mode w / h < 1.0 or if the app domain starts with m.. Since this is simply evaluated JavaScript code, literally anything can be used, eg. custom variables or function calls. Common variables like w, h, aspect are pre-defined and can be used as shown.

GLSL syntactic sugar

For shaders that are similar in WebGL 1 and 2, Ynet tries to convert often-used keywords such as in, out and gl_FragColor to their respective pendants. Furthermore single lines or code blocks can be tagged so Ynet will only include the matching parts:

uniform samplerCube in_sky; uniform vec3 in_ambient; in vec3 fs_texcoord; // varying in GL1 out vec4 out_color; // gl_FragColor in GL1 void main( void ) { <1> gl_FragColor = textureCube( in_sky, fs_coords ); // GL1 only <2> out_color = texture( in_sky, fs_ coords ); // GL2 only // Boost colors in GL1 only <1* gl_FragColor.rgb *= 1.5; *1> }

Ynet modules

client Ynet client (base framework, client-only). 60 KB
core Ynet core functionality (server and client). 160 KB
html HTML (creation, manipulation, dragging, language translation). 72 KB
image Image (creation, manipulation, drawing, color effects) and color class (RGB/CMYK/HSL/Greyscale, alpha channel). 22 KB
audio Sound effects and music playback (sound sprites, volume/pitch variations, reverb/distortion effects, seamless looping, crossfading, equalizer). 16 KB
login Account manager (FIDO2, password, e-mail, device) and session handler (server only). 46 KB
map 2D map class for editing (cut/copy/paste areas, insert/delete cols and rows, rotate/scale/flip/blur map, terrain generator) and tile-based games (wrap/endless mode, A* path finding). 27 KB
math Numerous math and geometry functions, randomization, vector / matrix classes. 44 KB
webgl WebGL 1 and 2 (shaders, textures, polygons, lights, shadows). 99 KB
voxel Voxel addon (loader, renderer, shaders). 54 KB
data Buffers (array_map, growbuffer, heap, ringbuffer) and interpolation (curve, slope, tween) 32 KB
+ Everything else (database, gamepad / keyboard, hashing, heartbeat / fps, resource loader) 54 KB

Please note that the listed sizes are taken from the source files. In practice much less data (usually around 200 KB in total) will be served due to trimming, compression and simply because some modules are never transmitted (db, session, etc.). Likewise some modules are client-only (html, gamepad, webgl, etc.).

Example: console chat

Screenshot

The following code is a complete example running a simple console-based chat. Users can enter a name, get a list of all connected users, get notified when someone joins or leaves, and can broadcast plain text messages. For simplicity, a user counts as logged in if the username property is set (otherwise the user is connected but didn't choose a name yet).

Note that the print event is built-in and text coloring is done through color tags (eg. <lr> means light red). The application is started by running node example/init.js on the server.


example/init.js

// Ynet starter require( "../core.js" ).start({ port:8000, includes:"chat" });

example/chat.js

// Create the Ynet $s/$c/$b scopes seen below core.createScopes( exports ); // $s means server code $s.module.init() { // Start listener core.listen(); } $s.core.onConnection( con ) { // Someone connected to the server // Add the username property con.username = null; // Now send the chat module (this file) and wait for input con.transmit( "chat" ); } $s.core.onDisconnect( con ) { // The connection to this user was lost var name = con.username; if( !name ) { return; // User was not logged in } // Logout by clearing the username con.username = null; // Broadcast who left (note that the "print" event is built-in) for( core.connections as c ) { if( c.username ) c.send( "print", "<lr>Left: " + name ); } } // $c means client code $c.module.create( initial ) { // The client module is ready if( initial ) { // Display a welcome message print( "<w>Welcome to the chat!" ); print( "<w>Enter your username first:" ); } } // And here's the beef, client sending and server receiving $c.core.onConsoleInput( data ) { // User pressed enter if( data.length < 1 ) { return; // Empty message } // Forward input to the server... core.send( "chatInput", data ); } $s.events.chatInput( data ) { // ...and here the server receives the input // Make sure we received a string of 2000 chars max. /// data = str (2k) // Trim spaces and sanitize HTML first data = data.trim().hss(); if( data.length < 1 ) { return; // Empty message } // Check login state if( this.username ) { // Name present, forward the message game.doForward( this, data ); } else { // No name set, try to login first game.doLogin( this, data ); } } $s.game.doLogin( con, data ) { // Only allow basic letters and numbers var name = data.replace( /[^a-zA-Z0-9]/g, "" ); if( name.length < 1 ) { // No valid chars left return con.send( "print", "<lg>Only invalid characters!" ); } if( name != data ) { // Send what is left return con.send( "print", "<lg>Try this instead: " + name ); } // Set username, user is logged in con.username = name; // Broadcast who joined for( core.connections as c ) { if( c.username ) c.send( "print", "<lg>Joined: " + name ); } // Collect a list of all usernames... var list = []; for( core.connections as c ) { if( c.username ) list.push( c.username ); } // ...and send it to the new user con.send( "print", "<lb>Users: " + list.join( ", " ) ); } $s.game.doForward( con, text ) { // Add name in yellow, content in grey var msg = "<y>" +con.username+ ": <e>" +text; // Broadcast message to everyone else for( core.connections as c ) { if( c.username && c != con ) c.send( "print", msg ); } }

Example: HTML checklist

Screenshot

The following code is a complete example running a simple session-based HTML checklist app. Users can add items and mark them as checked or unchecked. Data is stored within the user's session data (server-side) and restored when re-visiting the website (accessed through a session cookie).


example/init.js

// Ynet starter require( "../core.js" ).start({ port:8000, includes:"math math_random session html shared/reset shared/mains shared/bubbles shared/toggle_console html/$switch checklist" });

example/checklist.js

// Create the Ynet $s/$c/$b scopes seen below core.createScopes( exports ); $s.module.init() { // Start listener core.listen(); } $s.core.onConnection( con ) { // Serve all modules for( var name in core.modules ) con.transmit( name ); // Create list for this user if( !con.storage.checklist ) con.storage.checklist = []; // Send list on re-connect for( con.storage.checklist as item ) { con.send( "setupItem", item ); } } $css.main() { // Some CSS formatting body { color:#EEE; background:#081828; } main { position:absolute; left:0; top:0; right:0; bottom:0; scroll:y; } h1 { font-size:1.5em; } h2 { border-bottom:1px solid; } h1, h2 { font-weight:bold; margin:2rem; } p { margin:2rem; } main bubble { margin-left:2rem; } input, button { padding:0.5em 1em; color:#000; background:#EEE; border:1px solid; } // Make controls transparent while waiting for server reply .locked { opacity:0.25; } } $cm.create( initial ) { // Attach HTML structure if( initial ) html ( "/* <main> <h1> Checklist <p> Welcome to your session-based checklist! <h2> Items list <p.itemList> <h2> Add item <p.addContent> <text.txtLabel ph="Enter caption" next="cmdAdd"> <button assign="cmdAdd"> Add */" ) .append(); } $ui.cmdAdd() { // Drop old errors game.clearBubbles(); // Get text input var text = html( "input.txtLabel" ).textValue(); if( !text ) { // Error bubble return game.fieldBubble( "p.addContent", "error", "Missing", true ); } // Send add request core.send( "addEntry", text ); // Keep locked until server responds return false; } $se.addEntry( label ) { /// label = str (1-80) // Create entry var item = { id:randString(), label:label, checked:false }; // Store entry in session this.storage.checklist.push( item ); // Forward to client (after a delay to simulate lag) this.delay( "setupItem", item ); } $se.setChecked( id, state ) { /// id = str [base64] /// state = bool // Find item var item = this.storage.checklist.find_id( id ); if( !item ) { // Ignore invalid requests return; } // Update new data item.checked = state; // Notify the client (after a delay to simulate lag) this.delay( "setupItem", item ); } $ce.setupItem( item ) { // Find existing item var target = html( '.switch[id="' +item.id+ '"]' ); if( target ) { // Modify and unlock item target.checked( item.checked ).locked( false ); } else { // Create new item target = html( "<$switch>" ) .attr( "id", item.id ) .text( item.label ) .append( ".itemList" ) .checked( item.checked ) .assign( "onCheck" ); } // Unlock and clear form html( ".cmdAdd" ).locked( false ); html( ".txtLabel" ).value( "" ).focus( true ); } $ui.onCheck() { // Get new state var state = this.checked(); // Take back change because the server will apply it later this.checked( !state ); // Lock until server responds this.locked( true ); // Request change core.send( "setChecked", this.attr( "id" ), state ); }

Remarks: While in $ui.onCheck() it's necessary to lock the switch explicitly, no such statement can be found in $ui.cmdAdd(), but return false instead. The reason is that Ynet handles change element (such as the custom switch) differently than click elements (such as buttons) which get automatically locked for a short time on clicking (to prevent accidental double-clicks).

Example: WebGL voxel renderer

Screenshot

The following code is a complete example that loads a voxel graphic (room) and presents it in front of a skybox (the camera orbits slowly around). The renderer is configured to use WebGL 1 but could easily use WebGL 2 (by changing init.js accordingly) without any difference.


example/init.js

// Ynet starter require( "../core.js" ).start({ port:8000, includes:"math math_geometry math_vector math_matrix growbuffer html image resources webgl webgl_base webgl_shader webgl_texture webgl_voxel webgl_voxel_loader webgl_voxel_shader heart test/mod_camera_orbit demo_room", webgl:1 });

example/demo_room.js

// Create the Ynet $s/$c/$b scopes seen below core.createScopes( exports ); $s.module.init() { // Start listener core.listen(); } $s.core.onConnection( con ) { // Serve all modules for( var name in core.modules ) con.transmit( name ); } $css.main() { // Keep console visible body { display:flex; } div, terminal { position:relative; } if( ws ) { body { flex-direction:row; } div { width:70vw; height:100%; flex:1; } terminal { width:30vw; height:100%; border-right:1px solid; } } if( hs ) { body { flex-direction:column-reverse; } div { width:100%; height:70vh; flex:1; } terminal { width:100%; height:30vh; border-top:1px solid; } } canvas { width:100%; height:100%; } } $cm.init() { // Load rsources core.options.resPath = "//www.yhoko.com/res/ynet/"; game.imgSky = create( "texture" ).load( "desert.cube.png" ); game.imgRoom = create( "voxel" ).load( "ybureau.vox" ); // Camera game.camera = create( "camera_orbit" ) .setOrbit( 0.3, Tau34 ) .setPerspective( 20 ) .setZoom( 5 ); // Create canvas game.divFront = html( "<div>" ).attach(); game.imgFront = html( "<canvas>" ).attach( game.divFront ); // Initial resize core.onResize(); } $c.core.onResize() { // Pause while resizing if( heart.active ) heart.stop(); // Let the browser resize all elements first delay() { // Resize the output buffer aka. canvas size game.imgFront.applySize(); var w = game.imgFront.w; var h = game.imgFront.h; // Connect WebGL - if this fails the App can't run if( !core.initGL( game.imgFront ) ) { return core.err( "Could not connect WebGL" ); } // Resize (actually re-create) the internal screen buffer game.buffer = create( "renderTarget" ).setup( "color_depth", w,h ); // Resize camera game.camera.setSize( w,h ); // Resume after resizing if( !heart.active ) heart.start(); } } $c.global.onMain( delta, frametime ) { // Setup blue pulsing background color var bg = [ 0, 0.1+0.2*sin1(now/800), 0.2+0.3*sin1(now/1000) ]; // Open renderer and clear canvas if( !core.beginScene( bg ) ) { heart.stop(); core.log( "<err>Error: <e>Lost WebGL" ); return; } // Sample rotation game.camera.rz += 0.01 * delta; // Update camera and apply world transformation game.camera.update(); // Clearing the screen buffer is recommended game.buffer.clear(); // Make sure textures are loaded if( game.imgSky.ready && game.imgRoom.ready ) { // Render sky to screen core.renderScreen( null, "sky", null, { colors:game.imgSky, view:game.camera.matSky, ambient:[1,1,1] } ); var room = game.imgRoom; // Voxel shader setup var uniforms = { sunlight:[ 0,0.5,-1, 0.5 ] }; // x,y,z, intensity // Renderer setup; move room to world center var matrix = Matrix().translate( -room.w/2, -room.h/2, -room.d/4 ); // Render model to buffer room.render( game.buffer, matrix, uniforms ); // Copy buffer to screen core.renderScreen( game.buffer, "screenCopy", null ); } // Close renderer core.endScene(); }

Download

Not (yet) available to the public, sorry. This is a private project.

History

Ynet is the HTML5-based successor of the YDK framework, which was created around 2005 as a Visual Basic 6.0 convenience API. Large parts of the original YDK were DirectX wrappers, so one could easily render graphics (shapes, 2D images and later even 3D models), play sounds (variing pitch and volume), setup music (fading and looping automatically), handle input devices (including rumble effects) and many things more (networking, path finding, data encryption, scripting, etc.).

Unfortunately Microsoft decided to discontinue the DirectX extension for VB6 (the last version was DirectX 8.1) and even VB6 itself, in order to push their dotNet framework. After some roaming through the realms of early HTML5 (before WebGL was available), Java and even Flash, the final decision was to use HTML5 and a node.js server respectively. Finally, in the summer of 2020, the Ynet.dev project was born and here we are.

The lesson of this journey is to stick with standards, not companies.

Although I am very disappointed to see WebGL being discontinued in favour of the upcoming WebGPU API. I was strongly hoping to see geometry shaders in WebGL one day...