drawnui/drawnui.net/drawnui-web-app

Claude Code and agent skill by drawnui/drawnui.net

Trust tier 3
  • community
  • ext:[{"type":"plugin","description":"Custom plugin integration"}]
Source check pending

Checking that the SKILL.md file is still in the repository.

Scaffold a DrawnUi.Web app — pure WebAssembly (no Blazor) DrawnUI in the browser. Create a new standalone web app, or extract shared DrawnUI code from an existing app (MAUI/OpenTK) into a shared-source project and add a web head that references it. Also covers hunting intermittent WASM runtime bugs (frozen lotties, dead caches, Task.Run starvation) with probes + CPU throttling. Trigger on "drawnui web app", "drawnui.web", "drawnui.wasm", "DrawnUi.Wasm", "src/Wasm", "pure wasm drawnui", "run my drawnui app in browser", "extract drawnui shared code", "add web target to drawnui", "bug only on slow device", "lottie not playing in browser".

Install

npx vskill@latest install drawnui/drawnui.net/drawnui-web-app

Semi-extensibleMentions customization, but needs skill-specific knowledge. Read the extensible skills standard

SKILL.md

DrawnUi.Web App Scaffolding

Create or extend a pure-WebAssembly DrawnUI app (DrawnUi.Web package — NO Blazor, NO Razor, only [JSImport]/[JSExport]). Built on the DRAWNUI_NET base (SharedNet + Net shims), same base as OpenTK.

Load the `drawnui` skill for control/layout/caching rules and `drawnui-fluent` for code-behind composition. This skill covers ONLY the project plumbing.

Naming note: the library project is src/Wasm/DrawnUi/DrawnUi.Wasm.csproj. Its PackageId is `DrawnUi.Web` (also the static-asset base path), its assembly name is `DrawnUi.Wasm` (no AssemblyName override). So: PackageReference Include="DrawnUi.Web", _content/DrawnUi.Web/drawnui-web.js, but getAssemblyExports('DrawnUi.Wasm'). Game addon: package DrawnUi.Web.Game (src/Wasm/Addons/DrawnUi.Wasm.Game).

Canonical reference in the DrawnUi repo: shared-source src/Shared/Samples/Pong.Shared (Pong.Shared.shproj + .projitems) consumed by the web head src/Wasm/Samples/PongWeb (the only pure-web sample; the same shared code also runs in MauiPong, OpenTkPong, WpfPong). Docs: docs/articles/web/index.md + getting-started.md.


Decide the flow

  • New standalone web app → do PART A only.
  • Existing DrawnUI app (MAUI/OpenTK) → also runs in browser → do PART B (extract shared) then PART A (web head imports shared).

Core rule: shared code is SHARED-SOURCE, not a class library

DrawnUI base types differ per target via #if and per-head NuGet package (DrawnUi.Maui vs DrawnUi.Web vs DrawnUi.OpenTk). A normal class library would bind to ONE package and break the others.

Use a Shared Project (.shproj + .projitems): the .cs files compile INTO each head with that head's package + constants. This is why Pong.Shared is .shproj, not a .csproj.

Put in shared: scenes, controls, game logic, view models, the Canvas/scene factory. Keep platform entry points (Main/MauiProgram) in each head.


PART A — Web head (new app, or web head for shared code)

A1. csproj

Microsoft.NET.Sdk.BlazorWebAssembly SDK (for static-asset/host plumbing) but NO Blazor code. WasmBuildNative=true is required so emcc relinks with the GPU js-library (SkiaSharpInterop.js) that DrawnUi.Web flows in through buildTransitive (without it a NuGet consumer fails to link: undefined symbol: InterceptBrowserObjects).

xml
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<WasmBuildNative>true</WasmBuildNative>
<!-- DRAWNUI_NET = non-Blazor base; WEB/BROWSER = web defaults -->
<DefineConstants>$(DefineConstants);DRAWNUI_NET;WEB;NET;NET10;BROWSER</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DrawnUi.Web" Version="1.10.8.13" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.7" PrivateAssets="all" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="4.148.0" />
<PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="14.2.0" />
</ItemGroup>
<!-- Only with a ProjectReference to the DrawnUi source instead of the package: NuGet imports
buildTransitive automatically, a ProjectReference does not. -->
<!-- <Import Project="..\..\DrawnUi\buildTransitive\DrawnUi.Wasm.props" /> -->
<ItemGroup>
<WasmExtraConfig Include="emcc_flags">
<Value>-s ERROR_ON_UNDEFINED_SYMBOLS=0 -s ALLOW_MEMORY_GROWTH=1</Value>
</WasmExtraConfig>
</ItemGroup>
<!-- Only when consuming shared code (PART B). Relative path to the .projitems. -->
<Import Project="..\..\Shared\MyApp.Shared.projitems" Label="Shared" />
</Project>

Match the SkiaSharp/HarfBuzz native-asset versions to what DrawnUi.Web itself references (check src/Wasm/DrawnUi/DrawnUi.Wasm.csproj: SkiaSharp 4.148.0, HarfBuzzSharp 14.2.0 at 1.10.6.22) to avoid native mismatch; a lower direct HarfBuzz pin than the package's is a NuGet downgrade.

A2. wwwroot/index.html

One <canvas id> + the loader module. Set touch-action:none on the canvas as the pre-boot default: with Gestures = Enabled the library replaces it with the axes the page can scroll (vertical always inside an iframe), Lock keeps none.

html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My DrawnUI Web App</title>
<style>
body { margin:0; padding:0; overflow:hidden; }
#drawnui-canvas { width:100vw; height:100vh; display:block; touch-action:none; }
</style>
</head>
<body>
<canvas id="drawnui-canvas"></canvas>
<script type="module" src="./main.js"></script>
</body>
</html>
A3. wwwroot/main.js

Boots the runtime and calls the app's [JSExport] Main, wiring DrawnUI input/frame/resize. Copy the full reference from src/Wasm/Samples/PongWeb/wwwroot/main.js (showLoader / hideLoader / showError from drawnui-web.js, recursive Main lookup). Essential glue:

js
import { dotnet } from './_framework/dotnet.js';
import { setModuleExports } from './_content/DrawnUi.Web/drawnui-web.js';
globalThis.dotnet = dotnet;
const { getAssemblyExports, getConfig } = await dotnet
.withApplicationArgumentsFromQuery().create();
const lib = await getAssemblyExports('DrawnUi.Wasm'); // assembly name, not the package id
const Input = lib.DrawnUi.Draw.WebInput;
const Super = lib.DrawnUi.Draw.Super;
const Host = lib.DrawnUi.Draw.BrowserHost;
setModuleExports({
onBrowserFrame: Super.OnBrowserFrame, onCanvasResize: Host.OnCanvasResize,
onPointerDown: Input.OnPointerDown, onPointerMove: Input.OnPointerMove,
onPointerUp: Input.OnPointerUp, onPointerCancel: Input.OnPointerCancel,
onWheel: Input.OnWheel,
onContextMenu: Input.OnContextMenu, onKeyDown: Input.OnKeyDown, onKeyUp: Input.OnKeyUp,
});
const config = getConfig();
const app = await getAssemblyExports(config.mainAssemblyName);
// find [JSExport] Main in `app` and call it
A4. Program.cs
csharp
public static partial class Program
{
[JSExport]
public static Task Main() =>
Super.UseDrawnUi()
.ConfigureFonts(fonts => fonts.AddFont("fonts/MyFont.ttf", "MyFont"))
.ConfigureStyles(styles => { /* optional Style setters */ })
.RunAsync("drawnui-canvas", () => BuildScene()); // BuildScene lives in shared code
static Canvas BuildScene() => new Canvas
{
Gestures = GesturesMode.Enabled,
RenderingMode = RenderingModeType.Accelerated, // WebGL, auto-falls back to raster
HorizontalOptions = LayoutOptions.Fill,
VerticalOptions = LayoutOptions.Fill,
Content = /* your DrawnUI tree (ideally from shared project) */
};
}

RunAsync(elementId, factory) wires rendering, input, the rAF loop, resize, fonts and gestures.


PART B — Extract shared code from an existing DrawnUI app

B1. Create the shared project folder
src/Shared/
MyApp.Shared.shproj
MyApp.Shared.projitems
GlobalAliases.cs (optional, e.g. type aliases)
... moved scene/control/.cs files ...
B2. MyApp.Shared.shproj (one fresh GUID, reused below)
xml
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>PUT-A-NEW-GUID-HERE</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="MyApp.Shared.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>
B3. MyApp.Shared.projitems (SAME GUID; list EVERY shared .cs)
xml
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects Condition="'$(MSBuildVersion)' == '' Or '$(MSBuildVersion)' &lt; '16.0'">$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>PUT-A-NEW-GUID-HERE</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>MyApp</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Scene\MainScene.cs" />
<!-- add EVERY moved .cs file here; nothing auto-globs in a shared project -->
</ItemGroup>
</Project>
B4. Move code + add to projitems
  • Move scenes/controls/logic out of the existing head into src/Shared/.
  • Add each moved file as a <Compile Include=.../> in .projitems.
  • Expose a scene factory the heads call (e.g. public static Canvas BuildScene() or a SkiaControl subclass) instead of building the tree inside Main/MauiProgram.
B5. Import the shared project into EVERY head

Existing head (e.g. MAUI .csproj) and the new web head:

xml
<Import Project="..\..\Shared\MyApp.Shared.projitems" Label="Shared" />

(Relative path from each head to the .projitems.) The existing head keeps its DrawnUi.Maui/DrawnUi.OpenTk package; the web head adds DrawnUi.Web. Same source, compiled per target.

B6. Guard platform-specific code in shared files
csharp
#if BROWSER // DrawnUi.Web (and Blazor) — pure-managed web
#elif DRAWNUI_NET // Net / OpenTK desktop base
#else // MAUI heads
#endif

Audit moved code for MAUI-only APIs (MainThread, native handlers, FileSystem.OpenAppPackageFileAsync) and gate or abstract them.


Web-specific gotchas (carry from the drawnui skill)

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 90 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
409
Repository forks
39
7-day trend score
0

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Extension points

  • pluginCustom plugin integration

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/drawnui/drawnui.net/drawnui-web-app/badge)](https://verified-skill.com/skills/drawnui/drawnui.net/drawnui-web-app)

Something wrong with this skill or its page? Tell us.