Browse Source

* Pas2js side of fresnel API

Michaël Van Canneyt 5 months ago
parent
commit
589e374f1c

+ 1617 - 0
src/pas2js/fresnel.browser.pas2js.wasmapi.pp

@@ -0,0 +1,1617 @@
+{
+    This file is part of the Fresnel Library.
+    Copyright (c) 2025 by the FPC & Lazarus teams.
+
+    Pas2js Fresnel interface - Webassembly rendering API
+
+    See the file COPYING.modifiedLGPL.txt, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+{$mode objfpc}
+{$h+}
+{$modeswitch externalclass}
+{$modeswitch advancedrecords}
+
+{$DEFINE IMAGE_USEOSC}
+
+// This units implements the abstract base browser support for fresnel
+// - for everything in the main thread use fresnel.simple.pas2js.wasmapi
+// - for everything webworker use fresnel.web.pas2js.wasmapi
+
+unit fresnel.browser.pas2js.wasmapi;
+
+interface
+
+// Define this to disable API Logging altogether
+{$DEFINE NOLOGAPICALLS}
+
+uses
+  Classes, SysUtils, Types, system.UITypes,
+  Web, JS, wasienv, webassembly,
+  fresnel.keys,
+  fresnel.wasm.shared,
+  fresnel.shared.pas2js,
+  fresnel.messages.pas2js.wasmapi,
+  fresnel.menubuilder.pas2js.wasmapi,
+  fresnel.selfiesegmentation.pas2js;
+
+type
+
+  TWasmFresnelBrowserApi = class;
+
+  TTimerCallback = procedure (aCurrentTicks, aPreviousTicks : NativeInt);
+
+  { TWindowReference }
+
+  TWindowReference = class (TObject)
+  private
+    FIsPopup : Boolean;
+    FLeft, FTop : TFresnelFloat;
+    FWidth, FHeight : Integer;
+    FPixelRatio : TFresnelFloat;
+
+    FWindowTitle : TJSHTMLElement;
+
+    function MouseToEvent(aEvent: TJSMouseEvent; aMessageID: TWindowMessageID): TWindowEvent;
+
+    function DoWheel(aEvent: TJSEvent): Boolean;
+
+    function DoContextMenu(aEvent: TJSEvent): Boolean;
+
+    function DoPointerDown(aEvent: TJSEvent): Boolean;
+    function DoPointerUp(aEvent: TJSEvent): Boolean;
+    function DoPointerMove(aEvent: TJSEvent): Boolean;
+
+    // Apply (virtual) size (width, height, pixelRatio) to actual pixel size (canvas)
+    procedure ApplyCanvasResize;
+
+  public
+    API : TWasmFresnelBrowserApi;
+    WindowCanvasID : TWindowCanvasID;
+    CanvasContext : TJSCanvasRenderingContext2D;
+    Canvas : TJSHTMLCanvasElement;
+    CanvasParent : TJSHTMLElement;
+    FMenuBuilder : TMainMenuBuilder;
+
+    constructor Create(const aID : TWindowCanvasID; aAPI : TWasmFresnelBrowserApi; aCanvas : TJSHTMLCanvasElement; aParent : TJSHTMLElement);
+
+    procedure PrepareCanvas;
+    procedure RemoveCanvas;
+
+    property Left : TFresnelFloat read FLeft;
+    property Top : TFresnelFloat read FTop;
+    procedure SetPos(aLeft, aTop : TFresnelFloat);
+
+    property Width : Integer read FWidth;
+    property Height : Integer read FHeight;
+    procedure SetSize(aWidth, aHeight : TFresnelFloat);
+
+    property PixelRatio : TFresnelFloat read FPixelRatio;
+
+    property WindowTitle : TJSHTMLElement read FWindowTitle;
+    property MenuBuilder : TMainMenuBuilder read FMenuBuilder write FMenuBuilder;
+    property IsPopup : Boolean read FIsPopup write FIsPopup;
+  end;
+
+  { TVideoReference }
+
+  TVideoFrameCallback = reference to procedure (aTimeStamp : TJSDOMHighResTimeStamp; aSnapShot : TJSImageBitmap);
+
+  TVideoReference = class
+  private
+    FID : TVideoElementID;
+    FVideoElement : TJSHTMLVideoElement;
+    FCanvas : TJSHTMLCanvasElement; // lazy initialization
+    FRequestVideoFrameID : Integer;
+    FAPI : TWasmFresnelBrowserApi;
+
+  public
+    constructor Create(const aID : TVideoElementID; aAPI : TWasmFresnelBrowserApi);
+
+    property ID : TVideoElementID read FID;
+
+    procedure StartCapture(const aDeviceID : String; aWidth, aHeight : Integer;
+                           aNotifyFrameCallback : TVideoFrameCallback);
+    procedure StopCapture;
+    function  IsCapturing : Boolean;
+
+    // returns a Promise of an ImageBitmap
+    function TakeSnapshot : TJSPromise;
+
+  end;
+
+  { TWasmFresnelBrowserApi }
+
+  TWasmFresnelBrowserApi = class(TWasmFresnelSharedApi)
+  private
+    FWindowsParentRoot : TJSHTMLELement;
+    FWindowsCanvases : TJSMap;
+
+    FFocusedCanvas : TWindowReference;
+    FLastFocused : TWindowReference;
+
+    FKeyMap : TJSObject;
+
+    FMenuSupport : Boolean;
+
+    FVideoReferences : TJSMap;
+
+    FTimerID : NativeInt;
+    FTimerInterval : NativeInt;
+
+    FSelfieSegmentation : TFresnelSelfieSegmentation;
+
+    class var vWindowID : LongInt;
+    class var vNextMenuID : LongInt;
+    class var vNextVideoID : LongInt;
+
+    class var vKeymap : TJSObject;
+    class var vKeyCodeMap : TJSObject;
+
+  protected
+
+    function GetNewWindowID : TWindowCanvasID;
+
+    function CreateMenuBuilder(aParent: TJSHTMLELement): TMainMenuBuilder;
+
+    function AllocateWindowReference(aSizeX, aSizeY : LongInt; aPopup : Boolean; aPixelRatio : TFresnelFloat): TWindowReference;
+    function DeAllocateWindowReference(const aID: TWindowCanvasID) : TCanvasError;
+
+
+    procedure SetFocusedCanvas(AValue: TWindowReference);
+
+    property FocusedCanvas : TWindowReference read FFocusedCanvas write SetFocusedCanvas;
+
+    procedure DoTimerTick; virtual; abstract;
+
+    procedure DoEnumerateUserMedia(aCallbackWhenReady : TProc);
+
+    function AllocateVideoReference : TVideoReference;
+    function DeAllocateVideoElement(const aID : TVideoElementID) : TCanvasError;
+    function GetVideoReference(const aID : TVideoElementID) : TVideoReference;
+
+    FClipboardText : String;
+    FClipboardExpirationTimestamp : NativeInt;
+    FClipboardTextPending : Boolean;
+    FClipboardReadCallbacks : TJSArray;
+    const cClipboardTextTTLinMilliseconds = 1500;
+
+    // True if synchronous result is available in FClipboardText
+    function UpdateClipboardReadText : Boolean;
+    procedure DoClipboardReadText(const aCallbackWhenReady : TProc);
+    procedure HandleClipboardReadText(const aText : String);
+
+  public
+    constructor Create(aEnv : TPas2JSWASIEnvironment); override;
+
+    property WindowsParentRoot : TJSHTMLElement read FWindowsParentRoot write FWindowsParentRoot;
+    property MenuSupport : Boolean read FMenuSupport write FMenuSupport;
+
+    procedure InstallGlobalHandlers;
+
+    procedure StartTimerTick;
+    procedure StopTimerTick;
+
+    // Window
+    function GetWindowRef(const aID: TWindowCanvasID): TWindowReference;
+    function GetWindowContext2D(const aID : TWindowCanvasID) : TJSCanvasRenderingContext2D;
+
+    function AllocateWindowCanvas(aSizeX, aSizeY : LongInt; aIDPtr: TWasmPointer; aPixelRatioPtr: PFresnelFloat; aPopup : Boolean): TCanvasError;
+    function DeAllocateWindowCanvas(const aID: TWindowCanvasID): TCanvasError;
+
+    function SetWindowTitle(const aID: TWindowCanvasID; aTitle: TWasmPointer; aTitleLen: LongInt): TCanvasError;
+
+    function ShowHideWindow(const aID : TWindowCanvasID; aShow: Boolean; const aParentID: TWindowCanvasID): TCanvasError;
+
+    // Cursor
+
+    function SetCursor(aTextUTF16: TWasmPointer; aUTF16Size: LongInt) : TCanvasError;
+
+    // Event
+
+    procedure WakeMainThread;
+
+    // Menu
+
+    function AddMenuItem(const aCanvasID : TWindowCanvasID; aParentID : TMenuID; aCaption : TWasmPointer; aCaptionLen : LongInt; aData: TWasmPointer; aFlags : LongInt; aShortCut : LongInt; aMenuID : PMenuID) : TCanvasError;
+    function DeleteMenuItem(const aCanvasID : TWindowCanvasID; aMenuID : TMenuID) : TCanvasError;
+    function UpdateMenuItem(const aCanvasID : TWindowCanvasID; aMenuID : TMenuID; aFlags : LongInt; aShortCut : LongInt) : TCanvasError;
+
+    // Keys
+
+    function SetSpecialKeyMap(Map : TWasmPointer; aLen : LongInt) : TCanvasError;
+    function KeyNameToKeyCode(const aKey: String) : TKeyKind;
+
+    class function GetGlobalKeyMap: TJSObject;
+    class function KeyNameToKeyCode(aMap : TJSObject;aKey : string) : TKeyKind;
+    class function CreateSpecialKeyNameMap : TJSObject;
+    class function CreateSpecialKeyCodeMap : TJSObject;
+
+    // Key handlers are global
+
+    class procedure SetWindowEventFromKeyboardEvent(aWindowEvent : TWindowEvent; aKeyboardEvent : TJSKeyboardEvent); static;
+    function DoKeyDownEvent(aEvent: TJSEvent): Boolean;
+    function DoKeyUpEvent(aEvent: TJSEvent): Boolean;
+
+    // Click & enter/leave handlers to detect loss of focus.
+
+    procedure DoGlobalClick(aEvent: TJSEvent);
+    procedure DoGlobalEnter(aEvent: TJSEvent);
+    procedure DoGlobalLeave(aEvent: TJSEvent);
+    //
+    procedure DoBroadcast(aEvent: TJSEvent);
+
+    // Clipboard
+
+    function ClipboardReadText(aUTF16SizePtr, aDataUTF16 : TWasmPointer) : TCanvasError;
+    function ClipboardWriteText(aTextUTF16: TWasmPointer; aUTF16Size: LongInt): TCanvasError;
+
+    // UserMedia
+
+    function UserMediaStartCapture(aDeviceID_UTF16 : TWasmPointer; aDeviceID_UTF16Size : Integer;
+                                   aVideoID : TWasmPointer;
+                                   aResolutionWidth, aResolutionHeight : Integer;
+                                   aOptions : Integer
+                                   ) : TCanvasError; virtual;
+    function UserMediaStopCapture(aVideoID : TVideoElementID) : TCanvasError;
+    function UserMediaIsCapturing(aVideoID : TVideoElementID) : TCanvasError;
+
+  end;
+
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+implementation
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+
+// Clipboard paste keys get special treatment in DoKeyDown:
+// instead of being forwarded right away, the clipboard state is updated,
+// and then they are forwarded. This workaround the restriction of the browser
+// clipboard being asynchronous while FMX expects synchronous access
+// having the FMX thread wait on the asynchronous result is problematic as well
+// since clipboard access can be longish...
+type
+  TClipboardPasteKey = record
+    KeyCode, Shift : Integer;
+  end;
+
+const
+  VK_INSERT = 45;
+  cClipboardPasteKeys : array [0..1]  of TClipboardPasteKey = (
+    (KeyCode: Ord('V');  Shift: WASM_KEYSTATE_CTRL),
+    (KeyCode: VK_INSERT; Shift: WASM_KEYSTATE_SHIFT)
+  );
+
+// FlagsToMenuFlags
+//
+Function FlagsToMenuFlags(Flags : LongInt) : TMenuFlags;
+
+  procedure add(aFlag: LongInt; aMenuFlag : TMenuFlag);
+  begin
+    if (Flags and aFlag)=aFlag then
+      Include(Result,aMenuFlag);
+  end;
+
+begin
+  Result:=[];
+  Add(MENU_FLAGS_INVISIBLE,mfInvisible);
+  Add(MENU_FLAGS_CHECKED,mfChecked);
+  Add(MENU_FLAGS_RADIO,mfRadio);
+end;
+
+type
+  { TJSKeyNameObjectCreator }
+
+  TKeyNameObjectCreator = class(TSpecialKeyEnumerator)
+    FObj : TJSObject;
+    procedure EnumKey(aCode: Integer; aName: string); override;
+  end;
+
+procedure TKeyNameObjectCreator.EnumKey(aCode: Integer; aName: string);
+begin
+  FObj.Properties[aName]:=aCode;
+end;
+
+type
+  { TKeyCodeObjectCreator }
+
+  TKeyCodeObjectCreator = class(TSpecialKeyEnumerator)
+    FObj : TJSObject;
+    procedure EnumKey(aCode: Integer; aName: string); override;
+  end;
+
+procedure TKeyCodeObjectCreator.EnumKey(aCode: Integer; aName: string);
+begin
+  FObj.Properties[IntToStr(aCode)]:=aName;
+end;
+
+{ TWindowReference }
+
+// Create
+//
+constructor TWindowReference.Create(const aID: TWindowCanvasID; aAPI: TWasmFresnelBrowserApi; aCanvas: TJSHTMLCanvasElement; aParent: TJSHTMLElement);
+begin
+  Canvas := aCanvas;
+  canvasParent := aParent;
+  API := aAPI;
+  WindowCanvasID := aID;
+  PrepareCanvas;
+  if Assigned(aCanvas) then
+  begin
+    FWidth := aCanvas.width;
+    FHeight := aCanvas.height;
+  end;
+end;
+
+// PrepareCanvas
+//
+procedure TWindowReference.PrepareCanvas;
+begin
+  CanvasContext := TJSCanvasRenderingContext2D(Canvas.getcontext('2d'));
+
+  Canvas.AddEventListener('contextmenu', @DoContextMenu);
+
+  Canvas.AddEventListener('pointerdown', @DoPointerDown);
+  Canvas.AddEventListener('pointerup',   @DoPointerUp);
+  Canvas.AddEventListener('pointermove', @DoPointerMove);
+
+  Canvas.AddEventListener('wheel', @DoWheel);
+end;
+
+// RemoveCanvas
+//
+procedure TWindowReference.RemoveCanvas;
+begin
+  CanvasParent.remove();
+  Canvas := nil;
+  CanvasParent := nil;
+  CanvasContext := nil;
+end;
+
+// SetPos
+//
+procedure TWindowReference.SetPos(aLeft, aTop : TFresnelFloat);
+var
+  style  : TJSCSSStyleDeclaration;
+begin
+  style := CanvasParent.Style;
+  style.SetProperty('position', 'absolute');
+  style.SetProperty('width', 'fit-content');
+  style.SetProperty('left', TJSFresnelFloat(JSValue(aLeft)).toFixed(2) + 'px');
+  style.SetProperty('top', TJSFresnelFloat(JSValue(aTop)).toFixed(2) + 'px');
+  FLeft := aLeft;
+  FTop := aTop;
+end;
+
+// SetSize
+//
+procedure TWindowReference.SetSize(aWidth, aHeight : TFresnelFloat);
+var
+  newWidth, newHeight : Integer;
+begin
+  newWidth := Round(aWidth);
+  newHeight := Round(aHeight);
+  if (newWidth <> FWidth) or (newHeight <> FHeight) then
+  begin
+    FWidth := Round(aWidth);
+    FHeight := Round(aHeight);
+    ApplyCanvasResize;
+  end;
+end;
+
+// ApplyCanvasResize
+//
+procedure TWindowReference.ApplyCanvasResize;
+var
+  lWindowEvent : TWindowEvent;
+begin
+  // those are virtual pixels, the px suffix is there to deceive The Enemy
+  Canvas.style.setProperty('width', IntToStr(FWidth) + 'px');
+  Canvas.style.setProperty('height', IntToStr(FHeight) + 'px');
+  // actual pixels
+  Canvas.Width := Round(FWidth * FPixelRatio);
+  Canvas.Height := Round(FHeight * FPixelRatio);
+  // notify wasm so it can adjust the backing offscreen canvas
+
+  lWindowEvent := TWindowEvent.Create(WindowCanvasID, WASMSG_RESIZE);
+  lWindowEvent.Param0 := FWidth;
+  lWindowEvent.Param1 := FHeight;
+  lWindowEvent.Param2 := Round(FPixelRatio * 1000);
+  API.EnqueueEvent(lWindowEvent);
+end;
+
+// MouseToEvent
+//
+function TWindowReference.MouseToEvent(aEvent: TJSMouseEvent; aMessageID: TWindowMessageID): TWindowEvent;
+var
+  lShiftState : Integer;
+
+  procedure IncludeState(aState : TShiftStateEnum);
+  begin
+    lShiftState += (1 shl Ord(aState));
+  end;
+
+begin
+  Result := TWindowEvent.Create(Self.WindowCanvasID, aMessageID);
+  Result.param0 := Round(aEvent.OffsetX);
+  Result.param1 := Round(aEvent.OffsetY);
+
+  lShiftState := 0;
+
+  if aEvent.buttons <> 0 then
+  begin
+    // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
+    if (aEvent.buttons and  1) <> 0 then IncludeState(ssLeft);
+    if (aEvent.buttons and  2) <> 0 then IncludeState(ssRight);
+    if (aEvent.buttons and  4) <> 0 then IncludeState(ssMiddle);
+    if (aEvent.buttons and  8) <> 0 then IncludeState(ssExtra1);
+    if (aEvent.buttons and 16) <> 0 then IncludeState(ssExtra2);
+
+    if aEvent.detail = 2 then
+      IncludeState(ssDouble);
+  end;
+
+  if aEvent.altKey   then IncludeState(ssAlt);
+  if aEvent.ctrlKey  then IncludeState(ssCtrl);
+  if aEvent.shiftKey then IncludeState(ssShift);
+  if aEvent.metaKey  then IncludeState(ssMeta);
+
+  Result.param2 := lShiftState;
+
+  // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
+  // (note this doesnt folow the same ordering as buttons)
+  case aEvent.button of
+    0 : Result.Param3 := Ord(TMouseButton.mbLeft);
+    1 : Result.Param3 := Ord(TMouseButton.mbMiddle);
+    2 : Result.Param3 := Ord(TMouseButton.mbRight);
+    3 : Result.Param3 := Ord(TMouseButton.mbExtra1);
+    4 : Result.Param3 := Ord(TMouseButton.mbExtra2);
+  else
+    Result.Param3 := Ord(TMouseButton.mbLeft);
+  end;
+end;
+
+// DoWheel
+//
+function TWindowReference.DoWheel(aEvent: TJSEvent): Boolean;
+var
+  lWheelEvent : TJSWheelEvent absolute aEvent;
+  lWindowEvent : TWindowEvent;
+begin
+  Result:=True;
+
+  aEvent.PreventDefault;
+  aEvent.StopPropagation;
+
+  lWindowEvent := MouseToEvent(lWheelEvent, WASMSG_WHEELY);
+  case lWheelEvent.deltaMode of
+    0 : lWindowEvent.Param3 := Round(lWheelEvent.deltaY);
+    1 : lWindowEvent.Param3 := Round(lWheelEvent.deltaY*12); // arbitrary
+    2 : lWindowEvent.Param3 := Round(lWheelEvent.deltaY*600); // arbitrary
+  end;
+  API.EnqueueEvent(lWindowEvent);
+end;
+
+// DoContextMenu
+//
+function TWindowReference.DoContextMenu(aEvent: TJSEvent): Boolean;
+begin
+  aEvent.preventDefault();
+  // TODO : route to FMX context menu
+end;
+
+// DoPointerDown
+//
+function TWindowReference.DoPointerDown(aEvent: TJSEvent): Boolean;
+var
+  evt : TJSPointerEvent absolute aEvent;
+begin
+  if evt.pointerType <> 'mouse' then
+    Exit(False);
+
+  evt.preventDefault;
+  evt.currentTargetElement.setPointerCapture(evt.pointerId);
+
+  if not IsPopup then
+    API.FocusedCanvas := Self;
+  API.EnqueueEvent(MouseToEvent(evt, WASMSG_MOUSEDOWN));
+
+  Result := True;
+end;
+
+// DoPointerUp
+//
+function TWindowReference.DoPointerUp(aEvent: TJSEvent): Boolean;
+var
+  Evt : TJSPointerEvent absolute aEvent;
+begin
+  if evt.pointerType <> 'mouse' then Exit(False);
+  Result:=True;
+  API.EnqueueEvent(MouseToEvent(evt,WASMSG_MOUSEUP));
+  // capture release is automatic in pointerup
+end;
+
+// DoPointerMove
+//
+function TWindowReference.DoPointerMove(aEvent: TJSEvent): Boolean;
+var
+  evt : TJSPointerEvent absolute aEvent;
+begin
+  if evt.pointerType <> 'mouse' then Exit(False);
+  Result := True;
+  API.EnqueueEvent(MouseToEvent(evt, WASMSG_MOVE));
+end;
+
+// ---------------
+// --------------- TVideoReference ---------------
+// ---------------
+
+// Create
+//
+constructor TVideoReference.Create(const aID: TVideoElementID; aAPI: TWasmFresnelBrowserApi);
+begin
+  FID := aID;
+  FAPI := aAPI;
+  FVideoElement := TJSHTMLVideoElement(document.createElement('video'));
+  FVideoElement.setAttribute('autoplay',  '');
+  FVideoElement.setAttribute('playsinline', '');
+  FVideoElement.style.setProperty('display', 'none');
+end;
+
+// StartCapture
+//
+procedure TVideoReference.StartCapture(
+  const aDeviceID: String; aWidth, aHeight: Integer;
+  aNotifyFrameCallback : TVideoFrameCallback
+  );
+
+  procedure DoRequestVideoFrame(aNow : TJSDOMHighResTimeStamp; aMetaData : TJSVideoFrameMetaData);
+  begin
+    TakeSnapshot._then(
+      function (aValue : JSValue) : JSValue
+      begin
+        aNotifyFrameCallback(aNow, TJSImageBitmap(aValue));
+      end
+    );
+    FVideoElement.requestVideoFrameCallback(@DoRequestVideoFrame);
+  end;
+
+var
+  lConstraints : TJSObject;
+
+begin
+  StopCapture;
+
+  lConstraints := JS.new(['video', JS.new(['width', aWidth, 'height', aHeight, 'deviceId', aDeviceID])]);
+  window.navigator.mediaDevices.getUserMedia(lConstraints)._then(
+    function (aValue : JSValue) : JSValue
+    begin
+      FVideoElement.srcObject := TJSHTMLMediaStream(aValue);
+      FVideoElement.play;
+      if Assigned(aNotifyFrameCallback) then
+        FRequestVideoFrameID := FVideoElement.requestVideoFrameCallback(@DoRequestVideoFrame);
+    end
+  ).catch(
+    function (aValue : JSValue) : JSValue
+    begin
+      StopCapture;
+    end
+  );
+end;
+
+// StopCapture
+//
+procedure TVideoReference.StopCapture;
+var
+  tracks : TJSArray;
+begin
+  if FRequestVideoFrameID <> 0 then
+  begin
+    FVideoElement.cancelVideoFrameCallback(FRequestVideoFrameID);
+    FRequestVideoFrameID := 0;
+  end;
+  if JSValue(FVideoElement.srcObject) then
+  begin
+    tracks := FVideoElement.srcObject.getTracks;
+    asm tracks.forEach(track => track.stop()) end;
+    FVideoElement.srcObject := nil;
+  end;
+end;
+
+// IsCapturing
+//
+function TVideoReference.IsCapturing: Boolean;
+begin
+  if JSValue(FVideoElement.srcObject) then
+    Result := True
+  else result := False;
+end;
+
+// TakeSnapshot
+//
+function TVideoReference.TakeSnapshot: TJSPromise;
+begin
+  if FCanvas = nil then
+    FCanvas := TJSHTMLCanvasElement(document.createElement('canvas'));
+  if (FCanvas.width <> FVideoElement.videoWidth) or (FCanvas.height <> FVideoElement.videoHeight) then
+  begin
+    FCanvas.width := FVideoElement.videoWidth;
+    FCanvas.height := FVideoElement.videoHeight;
+  end;
+  FCanvas.getContextAs2DContext('2d').drawImage(FVideoElement, 0, 0);
+  Result := window.createImageBitmap(FCanvas);
+end;
+
+// ---------------
+// --------------- TWasmFresnelBrowserApi ---------------
+// ---------------
+
+// Create
+//
+constructor TWasmFresnelBrowserApi.Create(aEnv: TPas2JSWASIEnvironment);
+begin
+  inherited Create(aEnv);
+
+  FTimerInterval := 10;
+
+  FWindowsCanvases := TJSMap.new;
+  FVideoReferences := TJSMap.new;
+  FClipboardReadCallbacks := TJSArray.new;
+
+  InstallGlobalHandlers;
+end;
+
+// GetNewWindowID
+//
+function TWasmFresnelBrowserApi.GetNewWindowID : TWindowCanvasID;
+begin
+  Inc(vWindowID);
+  Result := vWindowID;
+end;
+
+// CreateMenuBuilder
+//
+function TWasmFresnelBrowserApi.CreateMenuBuilder(aParent : TJSHTMLELement): TMainMenuBuilder;
+begin
+  Result := TDefaultMainMenuBuilder.Create(Self, aParent);
+end;
+
+// AllocateWindowReference
+//
+function TWasmFresnelBrowserApi.AllocateWindowReference(aSizeX, aSizeY: LongInt; aPopup: Boolean; aPixelRatio : TFresnelFloat): TWindowReference;
+var
+  CMenu, CTitle, CParent : TJSHTMLElement;
+  lCanvasElement : TJSHTMLCanvasElement;
+
+  lWindowID : TWindowCanvasID;
+  idSuffix : String;
+begin
+  lWindowID := GetNewWindowID;
+  idSuffix := lWindowID.ToIDString;
+
+  CParent:=TJSHTMLElement(document.createElement('div'));
+  CParent.id := 'ffp' + idSuffix;
+  if aPopup then
+    CParent.className:='fresnel-popup'
+  else CParent.className:='fresnel-window';
+  CParent.style.setProperty('display', 'none');
+  CParent.style.setProperty('user-select', 'none');
+
+  WindowsParentRoot.AppendChild(CParent);
+
+  if not aPopup then
+  begin
+    CTitle := TJSHTMLElement(document.createElement('div'));
+    CTitle.id := 'fft' + idSuffix;
+    CTitle.className := 'fresnel-window-title';
+    CParent.AppendChild(CTitle);
+    if MenuSupport then
+    begin
+      CMenu:=TJSHTMLElement(document.createElement('div'));
+      CMenu.id := 'ffm' + idSuffix;
+      CMenu.className:='fresnel-window-menu';
+      CMenu.style.setProperty('display','block');
+      CParent.AppendChild(CMenu);
+    end;
+  end
+  else
+  begin
+    CTitle := TJSHTMLElement(WindowsParentRoot.QuerySelector('.fresnel-window-title'));
+  end;
+
+  lCanvasElement := TJSHTMLCanvasElement(document.createElement('CANVAS'));
+  lCanvasElement.id := 'ffc' + idSuffix;
+  lCanvasElement.className:='fresnel-window-client';
+  lCanvasElement.width := Round(aSizeX * aPixelRatio);
+  lCanvasElement.height := Round(aSizeY * aPixelRatio);
+  lCanvasElement.style.setProperty('display', 'block');
+  lCanvasElement.style.setProperty('width', IntToStr(aSizeX) + 'px');
+  lCanvasElement.style.setProperty('height', IntToStr(aSizeY) + 'px');
+  CParent.AppendChild(lCanvasElement);
+
+  Result := TWindowReference.Create(lWindowID, Self, lCanvasElement, CParent);
+  Result.FWindowTitle := CTitle;
+  Result.IsPopup := aPopup;
+  Result.FPixelRatio := aPixelRatio;
+
+  if MenuSupport then
+    Result.MenuBuilder := CreateMenuBuilder(CMenu);
+  FWindowsCanvases.&set(lWindowID, Result);
+  FOffscreenCanvases.&Set(lWindowID, Result.Canvas);
+end;
+
+// DeAllocateWindowReference
+//
+function TWasmFresnelBrowserApi.DeAllocateWindowReference(const aID: TWindowCanvasID) : TCanvasError;
+var
+  lWinRef : TWindowReference;
+begin
+  lWinRef := GetWindowRef(aID);
+  if lWinRef = nil then
+    Exit(ECANVAS_NOCANVAS);
+  lWinRef.RemoveCanvas;
+  FWindowsCanvases.delete(aID);
+  lWinRef.Free;
+  Result := ECANVAS_SUCCESS;
+end;
+
+// GetWindowRef
+//
+function TWasmFresnelBrowserApi.GetWindowRef(const aID: TWindowCanvasID): TWindowReference;
+var
+  jsRef : JSValue;
+  canvasRef : TWindowReference absolute jsRef;
+begin
+  Result := nil;
+  jsRef := FWindowsCanvases.get(aID);
+  if jsRef then
+    Result := canvasRef;
+end;
+
+// GetWindowContext2D
+//
+function TWasmFresnelBrowserApi.GetWindowContext2D(const aID: TWindowCanvasID): TJSCanvasRenderingContext2D;
+var
+  lWindowRef : TWindowReference;
+begin
+  lWindowRef := GetWindowRef(aID);
+  if lWindowRef <> nil then
+    Result := lWindowRef.CanvasContext
+  else
+  begin
+    Console.Warn('Fresnel: Unknown window ', aID);
+    Result := nil;
+  end;
+end;
+
+// SetFocusedCanvas
+//
+procedure TWasmFresnelBrowserApi.SetFocusedCanvas(AValue: TWindowReference);
+var
+  otherID : TWindowCanvasID;
+  evt : TWindowEvent;
+begin
+  if FFocusedCanvas = aValue then Exit;
+
+  otherID := Default(TWindowCanvasID);
+  if Assigned(FFocusedCanvas) then
+  begin
+    if Assigned(aValue) then
+      otherID := aValue.WindowCanvasID;
+    evt := TWindowEvent.Create(FFocusedCanvas.WindowCanvasID, WASMSG_DEACTIVATE);
+    evt.Param0 := otherID;
+    EnqueueEvent(evt);
+    otherID := FFocusedCanvas.WindowCanvasID;
+  end;
+  FFocusedCanvas := aValue;
+  if Assigned(FFocusedCanvas) then
+  begin
+    evt := TWindowEvent.Create(FFocusedCanvas.WindowCanvasID, WASMSG_ACTIVATE);
+    evt.Param0 := otherID;
+    EnqueueEvent(evt);
+  end;
+end;
+
+// StartTimerTick
+//
+procedure TWasmFresnelBrowserApi.StartTimerTick;
+begin
+  FTimerID := Window.setInterval(@DoTimerTick, FTimerInterval);
+end;
+
+// StopTimerTick
+//
+procedure TWasmFresnelBrowserApi.StopTimerTick;
+begin
+  Window.clearInterval(FTimerID);
+  FTimerID := 0;
+end;
+
+// InstallGlobalHandlers
+//
+procedure TWasmFresnelBrowserApi.InstallGlobalHandlers;
+begin
+  Document.Body.AddEventListener('keydown',    @DoKeyDownEvent);
+  Document.Body.AddEventListener('keyup',      @DoKeyUpEvent);
+  Document.Body.AddEventListener('click',      @DoGlobalClick);
+  Document.Body.AddEventListener('mouseleave', @DoGlobalLeave);
+  Document.Body.AddEventListener('mouseenter', @DoGlobalEnter);
+
+  Document.AddEventListener('broadcast',       @DoBroadcast);
+end;
+
+// AllocateWindowCanvas
+//
+function TWasmFresnelBrowserApi.AllocateWindowCanvas(
+  aSizeX, aSizeY: LongInt;
+  aIDPtr: TWasmPointer;
+  aPixelRatioPtr: PFresnelFloat;
+  aPopup : Boolean
+  ): TCanvasError;
+var
+  lRef : TWindowReference;
+  lPixelRatio : TFresnelFloat;
+  V: TJSDataView;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.AllocateWindowCanvas(%d,%d)',[SizeX,SizeY]);
+  {$ENDIF}
+
+  lPixelRatio := window.devicePixelRatio;
+
+  lRef := AllocateWindowReference(aSizeX, aSizeY, aPopup, lPixelRatio);
+
+  MemoryDataView.setint32(aIDPtr, lRef.WindowCanvasID, env.IsLittleEndian);
+  MemoryDataView.setFloat32(aPixelRatioPtr, lPixelRatio, env.IsLittleEndian);
+
+  Result := ECANVAS_SUCCESS;
+end;
+
+// DeAllocateWindowCanvas
+//
+function TWasmFresnelBrowserApi.DeAllocateWindowCanvas(const aID: TWindowCanvasID): TCanvasError;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.DeAllocateWindowCanvas(%d)',[aID]);
+  {$ENDIF}
+
+  Result := DeAllocateWindowReference(aID);
+end;
+
+// SetWindowTitle
+//
+function TWasmFresnelBrowserApi.SetWindowTitle(const aID: TWindowCanvasID; aTitle: TWasmPointer; aTitleLen: LongInt): TCanvasError;
+var
+  lWindowRef : TWindowReference;
+  lTitle : String;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SetWindowTitle(%d,''%s'')',[aID,S]);
+  {$ENDIF}
+  lTitle := GetUTF16FromMem(aTitle, aTitleLen);
+
+  if aID = 0 then
+    Document.title := lTitle
+  else
+  begin
+    lWindowRef := GetWindowRef(aID);
+    if lWindowRef = nil then
+      Exit(ECANVAS_NOCANVAS);
+    lWindowRef.WindowTitle.InnerText := lTitle;
+  end;
+  Result := ECANVAS_SUCCESS;
+end;
+
+// ShowHideWindow
+//
+function TWasmFresnelBrowserApi.ShowHideWindow(
+  const aID: TWindowCanvasID; aShow: Boolean; const aParentID: TWindowCanvasID
+  ): TCanvasError;
+var
+  Ref, ParentRef: TWindowReference;
+  ParentElement: TJSHTMLElement;
+  style  : TJSCSSStyleDeclaration;
+begin
+  Ref := GetWindowRef(aID);
+  if not Assigned(Ref) then
+    Exit(ECANVAS_NOCANVAS);
+
+  style := Ref.CanvasParent.Style;
+  if aShow then
+    style.SetProperty('display', 'block')
+  else style.SetProperty('display', 'none');
+
+  ParentRef := GetWindowRef(aParentID);
+  if Assigned(ParentRef) and Assigned(ParentRef.CanvasParent) then
+    ParentElement := ParentRef.CanvasParent
+  else ParentElement := TJSHTMLElement(Document.GetElementByID('desktop'));
+
+  if ParentElement <> nil then
+  begin
+    if Ref.CanvasParent.ParentNode <> ParentElement then
+      ParentElement.appendChild(Ref.CanvasParent);
+    if ParentRef <> nil then
+      style.SetProperty('transform', 'translateY(' + TJSFresnelFloat(JSValue(ParentRef.Canvas.offsetTop)).toFixed(2) + 'px)')
+  end
+  else
+  begin
+    style.SetProperty('transform', 'none');
+  end;
+
+  Result := ECANVAS_SUCCESS;
+end;
+
+// SetCursor
+//
+function TWasmFresnelBrowserApi.SetCursor(aTextUTF16: TWasmPointer; aUTF16Size: LongInt): TCanvasError;
+var
+  lCursor : String;
+begin
+  lCursor := GetUTF16FromMem(aTextUTF16, aUTF16Size);
+  WindowsParentRoot.style.setProperty('cursor', lCursor);
+end;
+
+// WakeMainThread
+//
+procedure TWasmFresnelBrowserApi.WakeMainThread;
+var
+  callback : JSValue;
+begin
+  // if we reach here, we're in the appropriate thread, wake up the wasm
+  callback := InstanceExports['__fresnel_main_thread_wake'];
+  if callback then
+    TProcedure(callback)();
+end;
+
+// AddMenuItem
+//
+function TWasmFresnelBrowserApi.AddMenuItem(const aCanvasID: TWindowCanvasID; aParentID: TMenuID; aCaption: TWasmPointer; aCaptionLen: LongInt;
+  aData: TWasmPointer; aFlags: LongInt; aShortCut: LongInt; aMenuID: PMenuID): TCanvasError;
+
+var
+  S : String;
+  Ref : TWindowReference;
+  lMenuID : TMenuID;
+  el : TJSHTMLElement;
+  lFlags : TMenuFlags;
+
+begin
+  S := GetUTF16FromMem(aCaption, aCaptionLen);
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('FresnelAPI.AddMenuItem(%d,"%s",%d,[%x],[%x])',[aCanvasID,S,aParentID,aData,aMenuID]);
+  {$ENDIF}
+  if not MenuSupport then
+    Exit(ECANVAS_NOMENUSUPPORT);
+  Ref := GetWindowRef(aCanvasID);
+  if not Assigned(Ref) then
+    Exit(ECANVAS_NOCANVAS);
+  if not Assigned(Ref.MenuBuilder)then
+    Exit(ECANVAS_NOMENUSUPPORT);
+
+  Inc(vNextMenuID);
+  lMenuID := vNextMenuID;
+
+  LFlags:=FlagsToMenuFlags(aFlags);
+  if Ref.MenuBuilder.AddMenuItem(aParentID,lMenuID,S,lFlags,aShortCut,aData)<>Nil then
+  begin
+    MemoryDataView.setInt32(aMenuID, lMenuID, Env.IsLittleEndian);
+    El:=TJSHTMLELement(Document.GetElementByID('ffm' + aCanvasID.ToIDString));
+    if assigned(el) then
+      el.style.removeProperty('display');
+  end;
+  Result:=ECANVAS_SUCCESS;
+end;
+
+// DeleteMenuItem
+//
+function TWasmFresnelBrowserApi.DeleteMenuItem(const aCanvasID : TWindowCanvasID; aMenuID: TMenuID): TCanvasError;
+var
+  Ref : TWindowReference;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('FresnelAPI.AddMenuItem(%d,%d)',[aCanvasID,aMenuID]);
+  {$ENDIF}
+  if not MenuSupport then
+    Exit(ECANVAS_NOMENUSUPPORT);
+  Ref:=GetWindowRef(aCanvasID);
+  if not assigned(Ref) then
+    Exit(ECANVAS_NOCANVAS);
+  if not Assigned(Ref.MenuBuilder)then
+    Exit(ECANVAS_NOMENUSUPPORT);
+  Ref.MenuBuilder.RemoveMenuItem(aMenuID);
+  Result:=ECANVAS_SUCCESS;
+end;
+
+// UpdateMenuItem
+//
+function TWasmFresnelBrowserApi.UpdateMenuItem(const aCanvasID : TWindowCanvasID; aMenuID: TMenuID; aFlags: LongInt; aShortCut: LongInt): TCanvasError;
+begin
+  Result:=ECANVAS_UNSPECIFIED;
+  console.log('TWasmFresnelApi.UpdateMenuItem not implemented');
+end;
+
+// SetSpecialKeyMap
+//
+function TWasmFresnelBrowserApi.SetSpecialKeyMap(Map: TWasmPointer; aLen: LongInt): TCanvasError;
+var
+  Ptr : TWasmPointer;
+  V : TJSDataView;
+  i,old,new : Integer;
+  jsname : jsValue;
+  name: string absolute jsname;
+  inv : TJSObject;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SetSpecialKeyMap([%x],%d)',[Map,aLen]);
+  {$ENDIF}
+  FKeyMap := CreateSpecialKeyNameMap;
+  V:=MemoryDataView;
+  Ptr:=Map;
+  inv:=CreateSpecialKeyCodeMap;
+  for I:=1 to aLen do
+  begin
+    old:=v.getInt32(Ptr,env.IsLittleEndian);
+    inc(Ptr,SizeInt32);
+    new:=v.getInt32(Ptr,env.IsLittleEndian);
+    inc(Ptr,SizeInt32);
+    jsname:=inv.Properties[IntToStr(old)];
+    if IsString(jsname) then
+      FKeyMap.Properties[name]:=New;
+  end;
+  Result := (ECANVAS_SUCCESS);
+end;
+
+// KeyNameToKeyCode
+//
+function TWasmFresnelBrowserApi.KeyNameToKeyCode(const aKey: String): TKeyKind;
+var
+  lMap : TJSObject;
+begin
+  lMap:=FKeyMap;
+  if lMap=Nil then
+    lmap:=GetGlobalKeyMap;
+  Result:=KeyNameToKeyCode(lMap,aKey);
+//  Writeln('Mapped ',aKey,' to ',TJSJSON.StringIfy(Result));
+end;
+
+// GetGlobalKeyMap
+//
+class function TWasmFresnelBrowserApi.GetGlobalKeyMap: TJSObject;
+begin
+  if vKeyMap = nil then
+    vKeyMap := CreateSpecialKeyNameMap;
+  Result := vKeyMap;
+end;
+
+// KeyNameToKeyCode
+//
+class function TWasmFresnelBrowserApi.KeyNameToKeyCode(aMap : TJSObject; aKey: string): TKeyKind;
+var
+  num: JSValue;
+begin
+  num:=aMap.Properties[aKey];
+  Result.IsSpecial:=isDefined(num);
+  if Result.IsSpecial then
+    Result.KeyCode:=Integer(Num)
+  else
+    Result.KeyCode:=TJSString(JSValue(aKey)).codePointAt(0);
+end;
+
+// CreateSpecialKeyNameMap
+//
+class function TWasmFresnelBrowserApi.CreateSpecialKeyNameMap: TJSObject;
+var
+  creator : TKeyNameObjectCreator;
+begin
+  Result := TJSObject.New;
+  creator := TKeyNameObjectCreator.Create;
+  try
+    creator.FObj := Result;
+    EnumSpecialKeys(@creator.EnumKey);
+  finally
+    creator.Free;
+  end;
+end;
+
+// CreateSpecialKeyCodeMap
+//
+class function TWasmFresnelBrowserApi.CreateSpecialKeyCodeMap: TJSObject;
+begin
+  Result:=TJSObject.New;
+  With TKeyCodeObjectCreator.Create do
+    try
+      FObj:=Result;
+      EnumSpecialKeys(@EnumKey);
+    finally
+      Free;
+    end;
+end;
+
+// SetWindowEventFromKeyboardEvent
+//
+class procedure TWasmFresnelBrowserApi.SetWindowEventFromKeyboardEvent(
+  aWindowEvent : TWindowEvent; aKeyboardEvent : TJSKeyboardEvent
+  );
+var
+  isNormalChar : Boolean;
+begin
+  isNormalChar :=     (Length(aKeyboardEvent.Key) = 1)
+                  and not (aKeyboardEvent.ctrlKey or aKeyboardEvent.altKey or aKeyboardEvent.metaKey);
+
+  if isNormalChar then begin
+
+    aWindowEvent.param0 := TJSString(aKeyboardEvent.Key).charCodeAt(0);
+    aWindowEvent.param1 := 1;
+
+  end else begin
+
+    aWindowEvent.param0 := Longint(TJSObject(aKeyboardEvent)['keyCode']);
+    aWindowEvent.param1 := 0;
+
+  end;
+
+  aWindowEvent.param2 := TFresnelHelper.EncodeKeyboardShiftState(aKeyboardEvent);
+  aWindowEvent.param3 := 0;
+end;
+
+// DoKeyDownEvent
+//
+function TWasmFresnelBrowserApi.DoKeyDownEvent(aEvent: TJSEvent): Boolean;
+var
+  evt : TWindowEvent;
+  keyEvent : TJSKeyboardEvent absolute aEvent;
+  //keyKind : TKeyKind;
+  i : Integer;
+begin
+  if FocusedCanvas = nil then Exit(False);
+
+  aEvent.preventDefault;
+  aEvent.cancelBubble := True;
+
+  evt := TWindowEvent.Create(FocusedCanvas.WindowCanvasID, WASMSG_KEYDOWN);
+  //keyKind := KeyNameToKeyCode(keyEvent.Key);
+  //evt.param0 := keyKind.KeyCode;
+  //evt.Param1 := Ord(keyKind.isSpecial);
+  //evt.param2 := TWindowReference.EncodeShiftState(keyEvent);
+  SetWindowEventFromKeyboardEvent(evt, keyEvent);
+
+  // if not a normal key
+  if evt.param1 = 0 then
+  begin
+    for i := 0 to High(cClipboardPasteKeys) do begin
+      if     (evt.param0 = cClipboardPasteKeys[i].KeyCode)
+         and (evt.param2 = cClipboardPasteKeys[i].Shift) then
+      begin
+        DoClipboardReadText(
+          procedure
+          begin
+            // TODO: consider putting all future events on backburner to preserve sequence for fast typers ?
+            EnqueueEvent(evt);
+          end
+        );
+        Exit(True);
+      end;
+    end;
+  end;
+
+  EnqueueEvent(evt);
+
+  Result := True;
+end;
+
+// DoKeyUpEvent
+//
+function TWasmFresnelBrowserApi.DoKeyUpEvent(aEvent: TJSEvent): Boolean;
+var
+  evt : TWindowEvent;
+  keyEvent : TJSKeyboardEvent absolute aEvent;
+  //KeyKind : TKeyKind;
+begin
+  if FocusedCanvas = nil then Exit(False);
+
+  evt := TWindowEvent.Create(FocusedCanvas.WindowCanvasID, WASMSG_KEYUP);
+  //KeyKind:=KeyNameToKeyCode(KeyEvent.Key);
+  //evt.param0:=KeyKind.KeyCode;
+  //evt.Param1:=Ord(KeyKind.isSpecial);
+  //evt.param2:=TWindowReference.EncodeShiftState(KeyEvent);
+  SetWindowEventFromKeyboardEvent(evt, keyEvent);
+
+  EnqueueEvent(Evt);
+
+  aEvent.preventDefault;
+  aEvent.cancelBubble:=True;
+
+  Result := True;
+end;
+
+// DoGlobalEnter
+//
+procedure TWasmFresnelBrowserApi.DoGlobalEnter(aEvent : TJSEvent);
+begin
+  FocusedCanvas := FLastFocused;
+end;
+
+// DoGlobalLeave
+//
+procedure TWasmFresnelBrowserApi.DoGlobalLeave(aEvent : TJSEvent);
+begin
+  FLastFocused := FocusedCanvas;
+  FocusedCanvas := nil;
+end;
+
+// DoGlobalClick
+//
+procedure TWasmFresnelBrowserApi.DoGlobalClick(aEvent : TJSEvent);
+begin
+  if aEvent.targetElement.closest('.fresnel-window') = nil then
+    FocusedCanvas := nil;
+end;
+
+// DoBroadcast
+//
+procedure TWasmFresnelBrowserApi.DoBroadcast(aEvent: TJSEvent);
+var
+  evt : TWindowEvent;
+begin
+  evt := TWindowEvent.Create(0, WASMSG_BROADCAST);
+  evt.param0 := LongInt(TJSObject(aEvent)['param0']);
+  EnqueueEvent(evt);
+  aEvent.preventDefault;
+end;
+
+// ClipboardReadText
+//
+function TWasmFresnelBrowserApi.ClipboardReadText(aUTF16SizePtr, aDataUTF16: TWasmPointer): TCanvasError;
+var
+  view : TJSDataView;
+  size : LongInt;
+begin
+  if UpdateClipboardReadText then
+  begin
+    view := MemoryDataView;
+    size := view.getInt32(aUTF16SizePtr, Env.IsLittleEndian);
+    view.setInt32(aUTF16SizePtr, Length(FClipboardText), Env.IsLittleEndian);
+
+    if size < Length(FClipboardText) then
+      Exit(EWASMEVENT_BUFFER_SIZE);
+
+    SetUTF16ToMem(aDataUTF16, FClipboardText);
+    Result := EWASMEVENT_SUCCESS;
+  end else Result := EWASMEVENT_TRY_AGAIN;
+end;
+
+// ClipboardWriteText
+//
+function TWasmFresnelBrowserApi.ClipboardWriteText(aTextUTF16: TWasmPointer; aUTF16Size: LongInt): TCanvasError;
+var
+  text : String;
+begin
+  text := GetUTF16FromMem(aTextUTF16, aUTF16Size);
+  try
+    window.navigator.ClipBoard.writeText(text);
+    Result := EWASMEVENT_SUCCESS;
+  except
+    Result := EWASMEVENT_ERROR;
+  end;
+end;
+
+// DoEnumerateUserMedia
+//
+procedure TWasmFresnelBrowserApi.DoEnumerateUserMedia(aCallbackWhenReady : TProc);
+
+  procedure StopAllTracks(aMediaStream : TJSMediaStream);
+  begin
+    aMediaStream.getTracks.forEach(
+      procedure (aTrack: TJSMediaStreamTrack)
+      begin
+        aTrack.stop;
+      end
+    );
+  end;
+
+  procedure EnumerateDevices(aCallbackWhenReady : TProc);
+  var
+    lEnumerationPromise : TJSPromise;
+  begin
+    lEnumerationPromise := Window.navigator.mediaDevices.enumerateDevices;
+    lEnumerationPromise._then(
+      function (aDevices: JSValue) : JSValue
+      var
+        lDevice : TJSMediaDeviceInfo;
+        lDeviceArray : TJSMediaDeviceInfoArray absolute aDevices;
+        lIniOutput : String;
+        I : Integer;
+      begin
+        // Generate INI formatted output
+        for I := 0 to lDeviceArray.Length - 1 do
+        begin
+          lDevice := lDeviceArray.Devices[I];
+
+          lIniOutput += '[Device' + IntToStr(I+1) + ']' + #13#10
+                     + 'kind=' + lDevice.kind + #13#10
+                     + 'deviceId=' + lDevice.deviceId + #13#10
+                     + 'groupId=' + lDevice.groupId + #13#10;
+
+          if TJSObject(lDevice)['label'] then
+            lIniOutput += 'label=' + lDevice.label_ + #13#10;
+
+          lIniOutput += #13#10;
+        end;
+        FEnumeratedUserMedia := lIniOutput;
+        aCallbackWhenReady();
+      end
+    ).catch(
+      function (aValue: JSValue) : JSValue
+      begin
+        FEnumeratedUserMedia := '[Error]' + #13#10 + 'message=Enumeration rejected' + #13#10;
+        aCallbackWhenReady();
+      end
+    );
+  end;
+
+begin
+  FEnumeratedUserMedia := '';
+  try
+    // Request permission first to access media devices
+    Window.navigator.mediaDevices.getUserMedia(JS.New(['video', true, 'audio', true]))._then(
+      function(aMediaStream: JSValue) : JSValue
+      begin
+        StopAllTracks(TJSMediaStream(aMediaStream));
+        EnumerateDevices(aCallbackWhenReady);
+      end
+    ).catch(
+      function (aValue: JSValue) : JSValue
+      begin
+        EnumerateDevices(
+          procedure
+          begin
+            if Copy(FEnumeratedUserMedia, 1, 7) <> '[Error]' then
+              FEnumeratedUserMedia += '[Error]' + #13#10 + 'message=GetUserMedia rejected';
+            aCallbackWhenReady();
+          end
+        );
+      end
+    );
+  except
+    on E: Exception do
+    begin
+      FEnumeratedUserMedia := '[Error]' + #13#10 + 'message=' + E.Message + #13#10 + 'name=' + E.ClassName;
+      aCallbackWhenReady();
+    end;
+  end;
+end;
+
+// AllocateVideoReference
+//
+function TWasmFresnelBrowserApi.AllocateVideoReference: TVideoReference;
+var
+  lID : TVideoElementID;
+begin
+  Inc(vNextVideoID);
+  lID := vNextVideoID;
+
+  Result := TVideoReference.Create(vNextVideoID, Self);
+
+  FVideoReferences.&set(lID, Result);
+end;
+
+// DeAllocateVideoElement
+//
+function TWasmFresnelBrowserApi.DeAllocateVideoElement(const aID: TVideoElementID): TCanvasError;
+var
+  lRef : TVideoReference;
+begin
+  lRef := GetVideoReference(aID);
+  if lRef <> nil then
+  begin
+    lRef.StopCapture;
+    FVideoReferences.delete(aID);
+    Result := ECANVAS_SUCCESS;
+  end else Result := ECANVAS_NOVIDEO;
+end;
+
+// GetVideoReference
+//
+function TWasmFresnelBrowserApi.GetVideoReference(const aID: TVideoElementID): TVideoReference;
+var
+  lJSRef : JSValue;
+begin
+  lJSRef := FVideoReferences.get(aID);
+  if lJSRef then
+    Exit(TVideoReference(lJSRef))
+  else Result := nil;
+end;
+
+// UpdateClipboardReadText
+//
+function TWasmFresnelBrowserApi.UpdateClipboardReadText : Boolean;
+begin
+  if FClipboardTextPending then
+    Exit(False);
+
+  if TJSDate.now <= FClipboardExpirationTimestamp then
+    Exit(True);
+
+  DoClipboardReadText(nil);
+end;
+
+// DoClipboardReadText
+//
+procedure TWasmFresnelBrowserApi.DoClipboardReadText(const aCallbackWhenReady : TProc);
+begin
+  if Assigned(aCallbackWhenReady) then
+    FClipboardReadCallbacks.push(aCallbackWhenReady);
+
+  if FClipboardTextPending then Exit;
+
+  FClipboardTextPending := True;
+  FClipboardText := '';
+  FClipboardExpirationTimestamp := TJSDate.now + cClipboardTextTTLinMilliseconds;
+
+  try
+    window.navigator.ClipBoard.readText._then(
+      TJSPromiseResolver(@HandleClipboardReadText),
+      function (aValue : JSValue) : JSValue
+      begin
+        HandleClipboardReadText('');
+        Console.error('UpdateClipboardReadText rejected ', aValue);
+      end
+    ).catch(
+      function (aValue : JSValue) : JSValue
+      begin
+        HandleClipboardReadText('');
+        Console.error('UpdateClipboardReadText catch ', aValue);
+      end
+    );
+  except
+    on E: Exception do
+    begin
+      HandleClipboardReadText('');
+      Console.error('UpdateClipboardReadText: ', E.Message);
+    end;
+  end;
+end;
+
+// HandleClipboardReadText
+//
+procedure TWasmFresnelBrowserApi.HandleClipboardReadText(const aText: String);
+var
+  i : Integer;
+  oldCallbacks : TJSArray;
+begin
+  Assert(FClipboardTextPending);
+  FClipboardTextPending := False;
+  FClipboardText := aText;
+  if FClipboardReadCallbacks.Length > 0 then
+  begin
+    oldCallbacks := FClipboardReadCallbacks;
+    FClipboardReadCallbacks := TJSArray.new;
+    for i := 0 to oldCallbacks.Length-1 do
+      TProc(oldCallbacks[i])();
+  end;
+end;
+
+// UserMediaStartCapture
+//
+function TWasmFresnelBrowserApi.UserMediaStartCapture(
+  aDeviceID_UTF16: TWasmPointer; aDeviceID_UTF16Size: Integer;
+  aVideoID: TWasmPointer;
+  aResolutionWidth, aResolutionHeight : Integer;
+  aOptions : Integer
+  ): TCanvasError;
+var
+  lVideoRef : TVideoReference;
+  lDeviceID : String;
+
+  procedure BlurCapture(aTimeStamp : TJSDOMHighResTimeStamp; aSnapShot : TJSImageBitmap);
+  var
+    lCanvasRef : TOffscreenCanvasReference;
+    lCallback : JSValue;
+    lBitmapID : Integer;
+  begin
+    lCallback := InstanceExports['__fresnel_usermedia_frame'];
+    if lCallback then
+    begin
+      if FSelfieSegmentation.IsReady then
+      begin
+        FSelfieSegmentation.Send(aSnapShot);
+        FSelfieSegmentation.OnResults :=
+          procedure (aResults : TJSSelfieSegmentationResults)
+          begin
+            lBitmapID := StoreImageBitmap(ProcessSegmentationResult(
+              aResults,
+              1, // maskThreshold,
+              7, // blurAmount,
+              0, // erosionDilationAmount,
+              0, // edgeEnhancementAmount,
+              5  // featheringAmount
+            ));
+            TUserMediaFrameCallback(lCallback)(aTimeStamp, lVideoRef.FID, lBitmapID);
+          end
+        ;
+      end;
+    end
+    else aSnapShot.close;
+  end;
+
+  procedure DirectCapture(aTimeStamp : TJSDOMHighResTimeStamp; aSnapShot : TJSImageBitmap);
+  var
+    lCanvasRef : TOffscreenCanvasReference;
+    lCallback : JSValue;
+    lBitmapID : Integer;
+  begin
+    lCallback := InstanceExports['__fresnel_usermedia_frame'];
+    if lCallback then
+    begin
+      lBitmapID := StoreImageBitmap(aSnapShot);
+      TUserMediaFrameCallback(lCallback)(aTimeStamp, lVideoRef.FID, lBitmapID);
+    end
+    else aSnapShot.close;
+  end;
+
+begin
+  lVideoRef := AllocateVideoReference;
+  try
+    lDeviceID := GetUTF16FromMem(aDeviceID_UTF16, aDeviceID_UTF16Size);
+
+    if (aOptions and cUserMediaCaptureOption_SelfieSegmentationBlur) <> 0 then
+    begin
+      if FSelfieSegmentation = nil then
+      begin
+        FSelfieSegmentation := TFresnelSelfieSegmentation.Create('/SelfieSegmentation');
+        FSelfieSegmentation.LoadModel(0);
+      end;
+      lVideoRef.StartCapture(lDeviceID, aResolutionWidth, aResolutionHeight, @BlurCapture);
+    end
+    else
+    begin
+      lVideoRef.StartCapture(lDeviceID, aResolutionWidth, aResolutionHeight, @DirectCapture)
+    end;
+    MemoryDataView.setInt32(aVideoID, lVideoRef.FID, Env.IsLittleEndian);
+    Result := ECANVAS_SUCCESS;
+  except
+    on E: Exception do
+    begin
+      Writeln('In UserMediaStartCapture, ', E.ClassName, ': ', E.Message);
+      DeAllocateVideoElement(lVideoRef.FID);
+      Result := ECANVAS_EXCEPTION;
+    end;
+  end;
+end;
+
+// UserMediaStopCapture
+//
+function TWasmFresnelBrowserApi.UserMediaStopCapture(aVideoID: TVideoElementID): TCanvasError;
+begin
+  Result := DeAllocateVideoElement(aVideoID);
+end;
+
+// UserMediaIsCapturing
+//
+function TWasmFresnelBrowserApi.UserMediaIsCapturing(aVideoID: TVideoElementID): TCanvasError;
+var
+  lRef : TVideoReference;
+begin
+  lRef := GetVideoReference(aVideoID);
+  if lRef = nil then
+    Exit(ECANVAS_NOVIDEO)
+  else if lRef.IsCapturing then
+    Exit(ECANVAS_SUCCESS)
+  else Result := ECANVAS_EXCEPTION;
+end;
+
+end.
+

+ 194 - 0
src/pas2js/fresnel.menubuilder.pas2js.wasmapi.pp

@@ -0,0 +1,194 @@
+{
+    This file is part of the Fresnel Library.
+    Copyright (c) 2025 by the FPC & Lazarus teams.
+
+    Pas2js Fresnel interface - menu builder interface.
+
+    See the file COPYING.modifiedLGPL.txt, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+{$mode objfpc}
+{$h+}
+{$modeswitch externalclass}
+{$modeswitch advancedrecords}
+
+{$DEFINE IMAGE_USEOSC}
+
+unit fresnel.menubuilder.pas2js.wasmapi;
+
+// Define this to disable API Logging altogether
+{$DEFINE NOLOGAPICALLS}
+
+interface
+
+uses
+  SysUtils,
+  Web,
+  fresnel.wasm.shared,
+  fresnel.shared.pas2js;
+
+ type
+
+  { TMainMenuBuilder }
+
+  TMenuClickCallback = procedure (aMenuID : TMenuID; aUserData : TWasmPointer);
+
+  TMenuFlag = (mfInvisible,mfChecked,mfRadio);
+  TMenuFlags = set of TMenuFlag;
+
+  TMainMenuBuilder = class(TObject)
+  private
+    FApi: TWasmFresnelSharedApi;
+    FMenuParent: TJSHTMLElement;
+  protected
+    Procedure DoMenuClick(aEvent : TJSEvent);
+    function DoAddMenuItem(aParentID,aMenuID : TMenuID; aCaption : String; Flags : TMenuFlags; ShortCut : LongInt; aData : TWasmPointer) : TJSHTMLElement; virtual; abstract;
+    function DoRemoveMenuItem(aMenuID : TMenuID) : Boolean; virtual; abstract;
+    function DoUpdateMenuItem(aMenuID : TMenuID; aFlags : TMenuFlags; aFlagsToUpdate : TMenuFlags) : Boolean; virtual; abstract;
+  public
+    Constructor Create(aApi : TWasmFresnelSharedApi; aMenuParent : TJSHTMLElement); virtual;
+    function AddMenuItem(aParentID,aMenuID : TMenuID; aCaption : String; Flags : TMenuFlags; ShortCut : LongInt; aData : TWasmPointer) : TJSHTMLElement;
+    function RemoveMenuItem(aMenuID : TMenuID) : Boolean;
+    function UpdateMenuItem(aMenuID : TMenuID; aFlags : TMenuFlags; aFlagsToUpdate : TMenuFlags) : Boolean;
+
+    property MenuParent : TJSHTMLElement read FMenuParent;
+    property API : TWasmFresnelSharedApi read FApi;
+  end;
+
+  { TDefaultMainMenuBuilder }
+
+  TDefaultMainMenuBuilder = class(TMainMenuBuilder)
+  protected
+    function FindMenuElement(aMenuID: Integer): TJSHTMLElement;
+    function DoAddMenuItem(aParentID, aMenuID: TMenuID; aCaption: String; Flags : TMenuFlags; ShortCut : LongInt; aData: TWasmPointer): TJSHTMLElement; override;
+    function DoRemoveMenuItem(aMenuID : TMenuID) : Boolean; override;
+    function DoUpdateMenuItem(aMenuID : TMenuID; aFlags : TMenuFlags; aFlagsToUpdate : TMenuFlags) : Boolean; override;
+  end;
+
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+implementation
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+
+{ TMainMenuBuilder }
+
+// Create
+//
+constructor TMainMenuBuilder.Create(aApi: TWasmFresnelSharedApi; aMenuParent: TJSHTMLElement);
+begin
+  FAPI:=aApi;
+  FMenuParent:=aMenuParent;
+end;
+
+function TMainMenuBuilder.AddMenuItem(aParentID,aMenuID: TMenuID; aCaption: String; Flags : TMenuFlags; ShortCut : LongInt; aData: TWasmPointer): TJSHTMLElement;
+begin
+  Result:=DoAddMenuItem(aParentID,aMenuID,aCaption,Flags,ShortCut,aData);
+end;
+
+function TMainMenuBuilder.RemoveMenuItem(aMenuID: TMenuID): Boolean;
+begin
+  Result:=DoRemoveMenuItem(aMenuID);
+end;
+
+function TMainMenuBuilder.UpdateMenuItem(aMenuID: TMenuID; aFlags: TMenuFlags; aFlagsToUpdate: TMenuFlags): Boolean;
+begin
+  Result:=DoUpdateMenuItem(aMenuID,aFlags,aFlagsToUpdate);
+end;
+
+procedure TMainMenuBuilder.DoMenuClick(aEvent: TJSEvent);
+var
+  S : String;
+  MenuID : integer;
+  UserData : TWasmPointer;
+  menuEl : TJSHTMLElement;
+begin
+  MenuEl:=TJSHTMLElement(aEvent.currentTargetHTMLElement.parentElement);
+  S:=MenuEl.dataset['menuId'];
+  MenuID:=StrToIntDef(S,-1);
+  S:=MenuEl.dataset['menuUserData'];
+  UserData:=StrToIntDef(S,-1);
+  if (UserData<>-1) and (MenuID<>-1) then
+    API.HandleMenuClick(MenuID,UserData);
+end;
+
+{ TDefaultMainMenuBuilder }
+
+function TDefaultMainMenuBuilder.FindMenuElement(aMenuID : Integer) : TJSHTMLElement;
+
+begin
+  Result:=TJSHTMLElement(MenuParent.querySelector('li[data-menu-id="'+IntToStr(aMenuID)+'"]'))
+end;
+
+function TDefaultMainMenuBuilder.DoAddMenuItem(aParentID,aMenuID: TMenuID; aCaption: String; Flags : TMenuFlags; ShortCut : LongInt; aData: TWasmPointer): TJSHTMLElement;
+
+var
+  CaptionEl,MenuEl,ListEl,parentEl : TJSHTMLElement;
+
+begin
+  Result:=nil;
+  if AParentID=0 then
+    ParentEl:=MenuParent
+  else
+    ParentEl:=FindMenuElement(aParentID);
+  if Not assigned(ParentEl) then exit;
+  ListEl:=TJSHTMLElement(ParentEl.QuerySelector('ul'));
+  if Not Assigned(ListEl) then
+    begin
+    ListEl:=TJSHTMLElement(Document.createElement('ul'));
+    if ParentEl=MenuParent then
+      ListEl.classList.Add('fresnel-mainmenu')
+    else
+      ListEl.classList.Add('fresnel-submenu');
+    ParentEl.appendChild(ListEl);
+    end;
+  MenuEl:=TJSHTMLElement(Document.CreateElement('li'));
+  ListEl.appendChild(MenuEl);
+  MenuEl.dataset.Map['menuId']:=IntToStr(aMenuID);
+  MenuEl.dataset.Map['menuUserData']:=IntToStr(aData);
+  if aCaption='-' then
+    begin
+    MenuEl.ClassList.add('fresnel-menu-separator');
+    CaptionEl:=TJSHTMLElement(Document.CreateElement('hr'))
+    end
+  else
+    begin
+    MenuEl.classList.Add('fresnel-menu-item');
+    CaptionEl:=TJSHTMLElement(Document.CreateElement('span'));
+    if mfChecked in Flags then
+      aCaption:=#$2611+' '+aCaption;
+    CaptionEl.InnerText:=aCaption;
+    CaptionEl.AddEventListener('click',@DoMenuClick);
+    end;
+  MenuEl.appendChild(CaptionEl);
+  Result:=MenuEl;
+end;
+
+function TDefaultMainMenuBuilder.DoRemoveMenuItem(aMenuID: TMenuID) : Boolean;
+
+var
+  El : TJSHTMLElement;
+
+begin
+  El:=FindMenuElement(aMenuID);
+  Result:=Assigned(El);
+  if Result then
+    El.parentElement.removeChild(El);
+end;
+
+function TDefaultMainMenuBuilder.DoUpdateMenuItem(aMenuID: TMenuID; aFlags: TMenuFlags; aFlagsToUpdate: TMenuFlags): Boolean;
+begin
+  Result:=False;
+end;
+
+
+
+end.
+

+ 155 - 0
src/pas2js/fresnel.messages.pas2js.wasmapi.pp

@@ -0,0 +1,155 @@
+{
+    This file is part of the Fresnel Library.
+    Copyright (c) 2025 by the FPC & Lazarus teams.
+
+    Pas2js Fresnel interface - Inter-worker messages API
+
+    See the file COPYING.modifiedLGPL.txt, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+{$mode objfpc}
+{$h+}
+{$modeswitch externalclass}
+{$modeswitch advancedrecords}
+
+{$DEFINE IMAGE_USEOSC}
+
+unit fresnel.messages.pas2js.wasmapi;
+
+interface
+
+uses
+ SysUtils, Types, Rtl.WorkerCommands,
+ JS, WebOrWorker,
+ fresnel.wasm.shared;
+
+const
+  cmdFresnel = 'fresnel';
+
+  // Atomic calls
+  cFresnel_Message_Call = 'fresnel_call';
+  cFresnel_Message_DOCOW = 'fresnel_docow';
+
+  // Fire & forget
+  cFresnel_EnqueueEvent = 'fresnel_enqueue_event';
+  cFresnel_RequestAnimationFrame = 'fresnel_raf';
+  cFresnel_Tick = 'fresnel_tick';
+  cFresnel_MenuClick = 'fresnel_menuclick';
+  cFresnel_EnumerateUserMedia = 'fresnel_usermedia_enum';
+  cFresnel_UserMediaFrame = 'fresnel_usermedia_frame';
+
+  // Host options
+  cFresnel_Message_MenuSupport = 'menu-support';
+
+type
+
+  { TFresnelMessage }
+
+  TFresnelMessage = class external name 'Object' (TCustomWorkerCommand)
+    Typ : String; external name 'type';
+  end;
+
+  { TFresnelMessageHelper }
+
+  TFresnelMessageHelper = class helper (TCustomWorkerCommandHelper) for TFresnelMessage
+    class function newMessage(aType : String) : TFresnelMessage; static;
+  end;
+
+  { TFresnelMessage_EnqueueEvent }
+
+  TFresnelMessage_EnqueueEvent = class external name 'Object' (TFresnelMessage)
+    Event : TJSObject; external name 'event';
+  end;
+
+  { TFresnelMessage_RequestAnimationFrame }
+
+  TFresnelMessage_RequestAnimationFrame = class external name 'Object' (TFresnelMessage)
+    Timestamp : Double; external name 'timestamp';
+    UserData : TWasmPointer; external name 'userData';
+  end;
+
+  { TFresnelMessage_EnumerateUserMedia }
+
+  TFresnelMessage_EnumerateUserMedia = class external name 'Object' (TFresnelMessage)
+    UserMediaData : String; external name 'userMediaData';
+    UserData : TWasmPointer; external name 'userData';
+  end;
+
+  { TFresnelMessage_UserMediaFrame }
+
+  TFresnelMessage_UserMediaFrame = class external name 'Object' (TFresnelMessage)
+    Timestamp : Double; external name 'timestamp';
+    VideoID : TVideoElementID; external name 'videoID';
+    ImageBitmap : TJSImageBitmap; external name 'imageBitmap';
+  end;
+
+  { TFresnelMessage_HandleMenuClick }
+
+  TFresnelMessage_HandleMenuClick = class external name 'Object' (TFresnelMessage)
+    MenuID : TMenuID; external name 'menuID';
+    UserData : TWasmPointer; external name 'userData';
+  end;
+
+  { TFresnelAtomicMessage }
+
+  TFresnelAtomicMessage = class external name 'Object' (TFresnelMessage)
+    ID : Integer; external name 'id';
+    Atomic : TJSInt32Array; external name 'atomic';
+  end;
+
+  { TFresnelMessage_FunctionCall }
+
+  TFresnelMessage_FunctionCall = class external name 'Object' (TFresnelAtomicMessage)
+    FuncName : String; external name 'funcName';
+    Args : TJSValueDynArray; external name 'args';
+    Memory : TJSArrayBuffer; external name 'memory';
+  end;
+
+  { TFresnelMessage_DrawOffscreenCanvasOnWindow }
+
+  TFresnelMessage_DrawOffscreenCanvasOnWindow = class external name 'Object' (TFresnelAtomicMessage)
+    WindowID : Integer; external name 'windowID';
+    ImageBitmap : TJSImageBitmap; external name 'imageBitmap';
+  end;
+
+  { TFresnelMessages }
+
+  TFresnelMessages = class abstract  // static class
+
+    class function CreateMessage_MenuSupport(const aValue : Boolean) : TFresnelMessage; static;
+
+  end;
+
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+implementation
+
+{ TFresnelMessageHelper }
+
+class function TFresnelMessageHelper.newMessage(aType: String): TFresnelMessage;
+begin
+  Result:=TFresnelMessage(createCommand(cmdFresnel));
+end;
+
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+
+{ TFresnelMessages }
+
+// CreateMessage_MenuSupport
+//
+class function TFresnelMessages.CreateMessage_MenuSupport(const aValue: Boolean): TFresnelMessage;
+begin
+  Result := TFresnelMessage.newMessage(cFresnel_Message_MenuSupport);
+  Result['value'] := aValue;
+end;
+
+end.
+

+ 18 - 4
src/pas2js/fresnel.pas2js.wasmapi.pp

@@ -1,3 +1,17 @@
+{
+    This file is part of the Fresnel Library.
+    Copyright (c) 2025 by the FPC & Lazarus teams.
+
+    Pas2js Fresnel interface - Webassembly rendering API
+
+    See the file COPYING.modifiedLGPL.txt, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
 {$mode objfpc}
 {$mode objfpc}
 {$h+}
 {$h+}
 {$modeswitch externalclass}
 {$modeswitch externalclass}
@@ -41,21 +55,21 @@ Type
   TFresnelHelper = Class
   TFresnelHelper = Class
   Private
   Private
     class var
     class var
-     _CurrentID : TCanvasID;
-     _CurrentMenuID : TMenuID;
+     _CurrentID : TOffscreenCanvasID;
+     _CurrentMenuID : Longint;
   Public
   Public
     Class function FresnelColorToHTMLColor(aColor : TCanvasColor) : string;
     Class function FresnelColorToHTMLColor(aColor : TCanvasColor) : string;
     Class function FresnelColorToHTMLColor(aRed, aGreen, aBlue, aAlpha: TCanvasColorComponent): string;
     Class function FresnelColorToHTMLColor(aRed, aGreen, aBlue, aAlpha: TCanvasColorComponent): string;
     class Function MouseButtonToShiftState(aButton: Integer): TShiftStateEnum;
     class Function MouseButtonToShiftState(aButton: Integer): TShiftStateEnum;
     Class Function ShiftStateToInt(aState : TShiftState) : Integer;
     Class Function ShiftStateToInt(aState : TShiftState) : Integer;
-    Class Function AllocateCanvasID : TCanvasID;
+    Class Function AllocateCanvasID : TOffscreenCanvasID;
     Class Function AllocateMenuID : TMenuID;
     Class Function AllocateMenuID : TMenuID;
   end;
   end;
 
 
   { TCanvasEvent }
   { TCanvasEvent }
 
 
   TCanvasEvent = record
   TCanvasEvent = record
-    CanvasID : TCanvasID;
+    CanvasID : TOffscreenCanvasID;
     msg : TCanvasMessageID;
     msg : TCanvasMessageID;
     param0 : TCanvasMessageParam;
     param0 : TCanvasMessageParam;
     param1 : TCanvasMessageParam;
     param1 : TCanvasMessageParam;

+ 393 - 0
src/pas2js/fresnel.selfiesegmentation.pas2js.pp

@@ -0,0 +1,393 @@
+{
+    This file is part of the Fresnel Library.
+    Copyright (c) 2025 by the FPC & Lazarus teams.
+
+    Pas2js Fresnel interface - Selfie-segmentation API
+
+    See the file COPYING.modifiedLGPL.txt, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+{$mode objfpc}
+{$h+}
+{$modeswitch externalclass}
+{$modeswitch advancedrecords}
+
+{$DEFINE IMAGE_USEOSC}
+
+unit fresnel.selfiesegmentation.pas2js;
+
+interface
+
+uses
+  SysUtils, Types, Math, JS, WeborWorker, Web;
+
+type
+
+  TJSSelfieSegmentationResults = class external name 'Object' (TJSObject)
+    image : TJSImageBitmap;
+    segmentationMask : TJSImageBitmap;
+  end;
+
+  TSelfieSegmentationResultsCallback = reference to procedure (aResults : TJSSelfieSegmentationResults);
+
+  TJSSelfieSegmentation = class external name 'SelfieSegmentation' (TJSObject)
+    constructor new(aOptions : TJSObject);
+    procedure setOptions(aOptions : TJSObject);
+    procedure onResults(aCallback : TSelfieSegmentationResultsCallback);
+    procedure initialize; async;
+    procedure send(aObject : TJSObject);
+  end;
+
+  { TFresnelSelfieSegmentation }
+
+  TFresnelSelfieSegmentation = class
+  private
+    FScript : TJSHTMLScriptElement;
+    FModel : TJSSelfieSegmentation;
+    FModelSelection : Integer;
+    FIsReady : Boolean;
+    FOnResults : TSelfieSegmentationResultsCallback;
+
+    procedure DoOnResults(aResults : TJSSelfieSegmentationResults);
+
+  public
+    constructor Create(const aBaseURL : String);
+
+    // 0 = portrait (general), 1 = landscape
+    procedure LoadModel(aModelSelection : Integer); async;
+
+    function IsReady : Boolean;
+
+    procedure Send(aImage : TJSObject);
+
+    property OnResults : TSelfieSegmentationResultsCallback read FOnResults write FOnResults;
+  end;
+
+// utility functions to be wrapped later in an object with a proper pipeline
+
+function ProcessSegmentationResult(
+  results: TJSSelfieSegmentationResults;
+  maskThreshold, blurAmount, erosionDilationAmount, edgeEnhancementAmount, featheringAmount : Double
+  ) : TJSImageBitmap;
+
+procedure ApplyFeathering(context: TJSCanvasRenderingContext2D; amount: Double);
+procedure EnhanceEdges(context: TJSCanvasRenderingContext2D; amount: Double);
+procedure ApplyThreshold(context: TJSCanvasRenderingContext2D; threshold: Double);
+procedure ApplyErosionDilation(context: TJSCanvasRenderingContext2D; amount: Double);
+
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+implementation
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+
+// ---------------
+// --------------- TFresnelSelfieSegmentation ---------------
+// ---------------
+
+// Create
+//
+constructor TFresnelSelfieSegmentation.Create(const aBaseURL: String);
+var
+  lScript : TJSHTMLScriptElement;
+begin
+  FModelSelection := 1;
+
+  lScript := TJSHTMLScriptElement(document.createElement('script'));
+  lScript.onload :=
+    function(Event: TEventListenerEvent): boolean
+    begin
+      FScript := lScript;
+      FModel := TJSSelfieSegmentation.new(JS.new([
+        'locateFile',
+        function (aFile : String) : String
+        begin
+          Result := aBaseURL + '/' + aFile;
+        end
+      ]));
+      if FModelSelection >= 0 then
+        LoadModel(FModelSelection);
+    end;
+  lScript.src := aBaseURL + '/selfie_segmentation.js';
+  document.body.append(lScript);
+end;
+
+// LoadModel
+//
+procedure TFresnelSelfieSegmentation.LoadModel(aModelSelection: Integer);
+
+  function DoLoad : Boolean; async;
+  begin
+    FModel.setOptions(JS.New(['modelSelection', aModelSelection]));
+    FModel.onResults(@DoOnResults);
+    FModel.initialize;
+    Result := True;
+  end;
+
+begin
+  FModelSelection := aModelSelection;
+  if FScript = nil then Exit;
+
+  FIsReady := AWait(DoLoad);
+end;
+
+// IsReady
+//
+function TFresnelSelfieSegmentation.IsReady: Boolean;
+begin
+  Result := (FScript <> nil) and FIsReady;
+end;
+
+procedure TFresnelSelfieSegmentation.Send(aImage: TJSObject);
+begin
+  FModel.send(JS.new(['image', aImage]));
+end;
+
+// DoOnResults
+//
+procedure TFresnelSelfieSegmentation.DoOnResults(aResults: TJSSelfieSegmentationResults);
+begin
+  if Assigned(FOnResults) then
+    FOnResults(aResults);
+end;
+
+// ---------------
+// --------------- Utility functions ---------------
+// ---------------
+
+// ProcessSegmentationResult
+//
+function ProcessSegmentationResult(
+  results: TJSSelfieSegmentationResults;
+  maskThreshold, blurAmount, erosionDilationAmount, edgeEnhancementAmount, featheringAmount : Double
+  ) : TJSImageBitmap;
+var
+  mask: TJSImageBitmap;
+  personCanvas, maskCanvas, bgCanvas: TJSHTMLCanvasElement;
+  canvas : TJSHTMLOffscreenCanvas;
+  personCtx, maskCtx, bgCtx: TJSCanvasRenderingContext2D;
+  ctx : TJSOffscreenCanvasRenderingContext2D;
+begin
+  canvas := TJSHTMLOffscreenCanvas.New(results.image.width, results.image.height);
+  ctx := canvas.getContextAs2DContext('2d');
+
+  ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+  mask := results.segmentationMask;
+
+  personCanvas := TJSHTMLCanvasElement(document.createElement('canvas'));
+  personCanvas.width := canvas.width;
+  personCanvas.height := canvas.height;
+  personCtx := TJSCanvasRenderingContext2D(personCanvas.getContext('2d'));
+
+  personCtx.drawImage(results.image, 0, 0, canvas.width, canvas.height);
+
+  maskCanvas := TJSHTMLCanvasElement(document.createElement('canvas'));
+  maskCanvas.width := canvas.width;
+  maskCanvas.height := canvas.height;
+  maskCtx := TJSCanvasRenderingContext2D(maskCanvas.getContext('2d'));
+
+  maskCtx.drawImage(mask, 0, 0, canvas.width, canvas.height);
+
+  ApplyThreshold(maskCtx, maskThreshold);
+
+  if erosionDilationAmount <> 0 then begin
+    ApplyErosionDilation(maskCtx, erosionDilationAmount);
+  end;
+
+  if edgeEnhancementAmount <> 0 then begin
+    EnhanceEdges(maskCtx, edgeEnhancementAmount);
+  end;
+
+  if featheringAmount <> 0 then begin
+    ApplyFeathering(maskCtx, featheringAmount);
+  end;
+
+  personCtx.globalCompositeOperation := 'destination-in';
+  personCtx.drawImage(maskCanvas, 0, 0, canvas.width, canvas.height);
+
+  bgCanvas := TJSHTMLCanvasElement(document.createElement('canvas'));
+  bgCanvas.width := canvas.width;
+  bgCanvas.height := canvas.height;
+  bgCtx := TJSCanvasRenderingContext2D(bgCanvas.getContext('2d'));
+
+  if blurAmount <> 0 then begin
+    bgCtx.filter := 'blur(' + FloatToStr(blurAmount) + 'px)';
+    bgCtx.drawImage(results.image, 0, 0, canvas.width, canvas.height);
+    bgCtx.filter := 'none';
+  end;
+
+  ctx.drawImage(bgCanvas, 0, 0);
+
+  ctx.drawImage(personCanvas, 0, 0);
+
+  Result := canvas.transferToImageBitmap;
+end;
+
+// ApplyFeathering
+//
+procedure ApplyFeathering(context: TJSCanvasRenderingContext2D; amount: Double);
+var
+  tempCanvas: TJSHTMLCanvasElement;
+  tempCtx: TJSCanvasRenderingContext2D;
+begin
+  context.filter := 'blur(' + FloatToStr(amount) + 'px)';
+
+  tempCanvas := TJSHTMLCanvasElement(document.createElement('canvas'));
+  tempCanvas.width := context.canvas.width;
+  tempCanvas.height := context.canvas.height;
+
+  tempCtx := TJSCanvasRenderingContext2D(tempCanvas.getContext('2d'));
+  tempCtx.drawImage(context.canvas, 0, 0);
+
+  context.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
+  context.drawImage(tempCanvas, 0, 0);
+  context.filter := 'none';
+end;
+
+// EnhanceEdges
+//
+procedure EnhanceEdges(context: TJSCanvasRenderingContext2D; amount: Double);
+var
+  imageData: TJSImageData;
+  data: TJSUint8ClampedArray;
+  width, height: Integer;
+  factor: Double;
+  original, edges: TJSUint8ClampedArray;
+  i, x, y: Integer;
+  center, left, right, top, bottom: Integer;
+  enhancedAlpha: Double;
+begin
+  width := context.canvas.width;
+  height := context.canvas.height;
+  imageData := context.getImageData(0, 0, width, height);
+  data := imageData.data;
+  factor := amount * 0.5;
+
+  original := TJSUint8ClampedArray.new(data);
+  edges := TJSUint8ClampedArray.new(data.length);
+
+  for y := 1 to height - 2 do begin
+    for x := 1 to width - 2 do begin
+      i := (y * width + x) * 4 + 3;
+
+      center := original[i];
+      left := original[(y * width + (x-1)) * 4 + 3];
+      right := original[(y * width + (x+1)) * 4 + 3];
+      top := original[((y-1) * width + x) * 4 + 3];
+      bottom := original[((y+1) * width + x) * 4 + 3];
+
+      edges[i] := Max(
+        Abs(center - left),
+        Max(Abs(center - right),
+        Max(Abs(center - top),
+        Abs(center - bottom)))
+      );
+    end;
+  end;
+
+  i := 3;
+  while i < data.length do begin
+    if edges[i] > 10 then begin
+      enhancedAlpha := original[i] + (edges[i] * factor);
+      data[i] := Min(255, Trunc(enhancedAlpha));
+    end;
+    Inc(i, 4);
+  end;
+
+  context.putImageData(imageData, 0, 0);
+end;
+
+// ApplyThreshold
+//
+procedure ApplyThreshold(context: TJSCanvasRenderingContext2D; threshold: Double);
+var
+  imageData: TJSImageData;
+  data: TJSUint8ClampedArray;
+  i, width, height: Integer;
+begin
+  width := context.canvas.width;
+  height := context.canvas.height;
+  imageData := context.getImageData(0, 0, width, height);
+  data := imageData.data;
+
+  i := 3;
+  while i < data.length do begin
+    if data[i] / 255 < threshold then
+      data[i] := 0
+    else
+      data[i] := 255;
+
+    Inc(i, 4);
+  end;
+
+  context.putImageData(imageData, 0, 0);
+end;
+
+// ApplyErosionDilation
+//
+procedure ApplyErosionDilation(context: TJSCanvasRenderingContext2D; amount: Double);
+var
+  width, height: Integer;
+  imageData: TJSImageData;
+  data, output: TJSUint8ClampedArray;
+  kernelSize: Integer;
+  isErosion: Boolean;
+  x, y, kx, ky, pixelIndex: Integer;
+  targetX, targetY, targetIndex: Integer;
+  minValue, maxValue: Integer;
+  newImageData: TJSImageData;
+begin
+  width := context.canvas.width;
+  height := context.canvas.height;
+  imageData := context.getImageData(0, 0, width, height);
+  data := imageData.data;
+  output := TJSUint8ClampedArray.new(data);
+
+  kernelSize := Max(1, Trunc(amount/2));
+  isErosion := amount < 0;
+
+  for y := 0 to height - 1 do begin
+    for x := 0 to width - 1 do begin
+      pixelIndex := (y * width + x) * 4 + 3;
+
+      if isErosion then begin
+        minValue := 255;
+        for ky := -kernelSize to kernelSize do begin
+          for kx := -kernelSize to kernelSize do begin
+            targetY := Min(height - 1, Max(0, y + ky));
+            targetX := Min(width - 1, Max(0, x + kx));
+            targetIndex := (targetY * width + targetX) * 4 + 3;
+            minValue := Min(minValue, data[targetIndex]);
+          end;
+        end;
+        output[pixelIndex] := minValue;
+      end else begin
+        maxValue := 0;
+        for ky := -kernelSize to kernelSize do begin
+          for kx := -kernelSize to kernelSize do begin
+            targetY := Min(height - 1, Max(0, y + ky));
+            targetX := Min(width - 1, Max(0, x + kx));
+            targetIndex := (targetY * width + targetX) * 4 + 3;
+            maxValue := Max(maxValue, data[targetIndex]);
+          end;
+        end;
+        output[pixelIndex] := maxValue;
+      end;
+    end;
+  end;
+
+  newImageData := TJSImageData.new(output, width, height);
+  context.putImageData(newImageData, 0, 0);
+end;
+
+
+end.
+

+ 2174 - 0
src/pas2js/fresnel.shared.pas2js.pp

@@ -0,0 +1,2174 @@
+{
+    This file is part of the Fresnel Library.
+    Copyright (c) 2025 by the FPC & Lazarus teams.
+
+    Pas2js Fresnel interface - Webassembly rendering API, shared between worker and main page
+
+    See the file COPYING.modifiedLGPL.txt, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+{$mode objfpc}
+{$h+}
+{$modeswitch externalclass}
+{$modeswitch advancedrecords}
+
+{$DEFINE IMAGE_USEOSC}
+
+unit fresnel.shared.pas2js;
+
+// APIs and functionality that is common to Web & WebWorkers
+
+interface
+
+// Define this to disable API Logging altogether
+{$DEFINE NOLOGAPICALLS}
+
+uses
+  SysUtils, Types,
+  wasienv, JS, WebOrWorker, webassembly,
+  fresnel.wasm.shared,
+  importextensionex.pas2js.wasmapi;
+
+type
+  TWasmFresnelSharedApi = class;
+
+  TWasmPointer = LongInt;
+
+  TTimerTickCallback = function (aTimerID : TTimerID; UserData : TWasmPointer) : Boolean;
+  TAnimationFrameCallback = procedure;
+  TUserMediaCallback = procedure (aUTF16Size : Integer; aUserData : TWasmPointer);
+  TUserMediaFrameCallback = procedure (aTimeStamp : TFresnelFloat; aVideoID : TVideoElementID; aImageBitmapID : Integer);
+  TProcessMessagesCallback = procedure (aCurrentTicks, aPreviousTicks : NativeInt);
+
+  TKeyKind = record
+    isSpecial : Boolean;
+    KeyCode : LongInt;
+  end;
+
+  TJSFresnelFloat = class external name 'Number'
+    function toFixed(aDigits : Integer) : string;
+  end;
+
+  { TFresnelHelper }
+
+  TFresnelHelper = class
+  public
+    class function FresnelColorToHTMLColor(aColor : TCanvasColor) : string; static;
+    class function FresnelColorToHTMLColor(aRed, aGreen, aBlue, aAlpha: TCanvasColorComponent): string; static;
+    class function RGBAToHTMLColor(aRed, aGreen, aBlue, aAlpha: Integer): string; static;
+
+    class function EncodeKeyboardShiftState(keyEvent: TJSObject): LongInt; static;
+  end;
+
+  { TOffscreenCanvasReference }
+
+  TOffscreenCanvasReference = class (TObject)
+  public
+    API : TWasmFresnelSharedApi;
+    OffscreenCanvasID : TOffscreenCanvasID;
+    CanvasContext : TJSOffscreenCanvasRenderingContext2D;
+    Canvas : TJSHTMLOffscreenCanvas;
+    Scale : TFresnelFloat;
+
+    FontOffsets : TJSMap;
+    FontOffset : TFresnelFloat;
+
+    constructor Create(const aID : TOffscreenCanvasID; aAPI : TWasmFresnelSharedApi; aWidth, aHeight: LongInt; aScale : TFresnelFloat);
+    procedure Resize(aWidth, aHeight: Longint; aScale: TFresnelFloat);
+    procedure RemoveCanvas;
+  end;
+
+  { TWindowEvent }
+
+  TWindowEvent = class (TJSObject)
+  public
+    WindowID : TWindowCanvasID;
+    Msg : TWindowMessageID;
+    Param0 : TWindowMessageParam;
+    Param1 : TWindowMessageParam;
+    Param2 : TWindowMessageParam;
+    Param3 : TWindowMessageParam;
+    NextEvent : TWindowEvent;
+
+    class function Create(const aWindowID : TWindowCanvasID; aMsg : TWindowMessageID) : TWindowEvent; static;
+  end;
+
+  { TWasmFresnelSharedApi }
+
+  TDebugAPI = (daText,daClipRect);
+  TDebugAPIs = Set of TDebugAPI;
+
+  TWasmFresnelSharedApi = class (TImportExtensionEx)
+  private
+    FCreateDefaultCanvas: Boolean;
+    FDefaultCanvas : TOffscreenCanvasReference;
+
+    FDebugAPIs: TDebugAPIs;
+    FLogAPICalls : Boolean;
+
+    FEventQueueHead : TWindowEvent;
+    FEventQueueTail : TWindowEvent;
+    FEventCount : Integer;
+    FUseWordColors: Boolean;
+    class var vEventRecycler : TWindowEvent;
+
+    class var vLastOffscreenCanvasID : LongInt;
+    class var vLastImageBitmapID : LongInt;
+
+  protected
+    // reference to the import object, currently used to whitelist calls
+    FImportObject : TJSObject;
+
+    FOffscreenCanvases : TJSMap;
+    FGradients : TJSMap;
+    FImageBitmaps : TJSMap;
+
+    FEnumeratedUserMedia : String; // in IniFile format
+
+    FProcessMessagesCallback : JSValue;
+    FLastTick : NativeInt;
+
+    procedure LogCall(const Msg{%H-} : String);
+    procedure LogCall(Const Fmt{%H-} : String; const Args{%H-} : Array of const);
+
+    // call of one of the FImportObject functions
+    function ExecuteFunctionCall(const funcName: string; const args: TJSValueDynArray): TCanvasError;
+    // call main_thread_wake, only do this from the main thread
+    procedure MainThreadWake;
+
+
+    function StoreImageBitmap(const aImageBitmap : TJSImageBitmap) : Integer;
+    function DeleteImageBitmap(const aID : Integer) : Boolean;
+    function GetImageBitmap(const aID : Integer) : TJSImageBitmap;
+
+    procedure SetCreateDefaultCanvas(AValue: Boolean);
+
+    property EventCount : Integer read FEventCount;
+    procedure EnqueueEvent(aEvent : TWindowEvent); virtual;
+    function DequeueEvent : TWindowEvent;
+    class function NewEvent(aWindowID : TWindowCanvasID; aMessageID : TWindowMessageID) : TWindowEvent; static;
+    class procedure RecycleEvent(aEvent : TWindowEvent); static;
+
+    procedure ProcessMessages;
+
+  public
+    constructor Create(aEnv : TPas2JSWASIEnvironment); override;
+
+    property LogAPICalls : Boolean read FLogAPICalls write FLogAPICalls;
+
+    procedure FillImportObject(aObject : TJSObject); override;
+    function ImportName : String; override;
+
+    property DebugAPIs : TDebugAPIs read FDebugAPIs write FDebugAPIs;
+
+    property DefaultCanvas : TOffscreenCanvasReference read FDefaultCanvas;
+    property CreateDefaultCanvas : Boolean read FCreateDefaultCanvas Write SetCreateDefaultCanvas;
+    // Get info
+
+    function GetOffscreenCanvasRef(const aID: TOffscreenCanvasID): TOffscreenCanvasReference;
+    function GetOffscreenContext2D(const aID : TOffscreenCanvasID) : TJSOffscreenCanvasRenderingContext2D;
+    // Menu
+
+    function HandleMenuClick(aMenuID : TMenuID; aData : TWasmPointer) : Boolean; virtual; abstract;
+
+    // Debug
+
+    procedure DrawBaseLine(C: TJSOffscreenCanvasRenderingContext2D; S: String; X, Y: Double);
+    procedure DrawClipRect(aRef: TOffscreenCanvasReference; aX, aY, aWidth, aHeight: double);
+
+    // OffscreenCanvas
+
+    function AllocateOffscreenCanvas(aSizeX, aSizeY : LongInt; aScale : TFresnelFloat; aBitmap : TWasmPointer; aID: TWasmPointer): TCanvasError;
+    function DeAllocateOffscreenCanvas(const aID: TOffscreenCanvasID): TCanvasError;
+    function ResizeOffscreenCanvas(const aID: TOffscreenCanvasID; aSizeX, aSizeY : Longint; aScale : TFresnelFloat): TCanvasError;
+
+    function MoveTo(const aID : TOffscreenCanvasID; aX, aY : TFresnelFloat): TCanvasError;
+    function LineTo(const aID : TOffscreenCanvasID; aX, aY : TFresnelFloat):  TCanvasError;
+    function Stroke(const aID : TOffscreenCanvasID): TCanvasError;
+    function BeginPath(const aID : TOffscreenCanvasID):  TCanvasError;
+    function Arc(const aID : TOffscreenCanvasID; aX, aY, aRadiusX, aRadiusY, aStartAngle, aEndAngle, aRotate : TFresnelFloat; aFlags : LongInt):  TCanvasError;
+    function FillRect(const aID : TOffscreenCanvasID; X, Y, Width, Height : TFresnelFloat): TCanvasError;
+    function StrokeRect(const aID : TOffscreenCanvasID; X, Y, Width, Height : TFresnelFloat ):  TCanvasError;
+    function ClearRect(const aID : TOffscreenCanvasID; X, Y, Width, Height : TFresnelFloat ):  TCanvasError;
+    function RoundRect(const aID : TOffscreenCanvasID; Flags : LongInt; Data : PFresnelFloat) : TCanvasError;
+    function StrokeText(const aID : TOffscreenCanvasID; X, Y : TFresnelFloat; aText : TWasmPointer; aTextLen : LongInt ):  TCanvasError;
+    function FillText(const aID : TOffscreenCanvasID; aX, aY : TFresnelFloat; aText : TWasmPointer; aTextLen : LongInt; aOpacity : TFresnelFloat):  TCanvasError;
+    function SetFillStyle(const aID : TOffscreenCanvasID; aRed,aGreen,aBlue,aAlpha: TCanvasColorComponent): TCanvasError;
+    function SetFillStyleString(const aID : TOffscreenCanvasID; aTextPtr : TWasmPointer; aTextLen : LongInt): TCanvasError;
+    function SetLinearGradientFillStyle(const aID : TOffscreenCanvasID; aStartX, aStartY, aEndX, aEndY : TFresnelFloat; aColorPointCount : LongInt; aColorPoints : TWasmPointer) : TCanvasError;
+    function SetImageFillStyle(const aID : TOffscreenCanvasID; Flags : LongInt; aImageWidth, aImageHeight: LongInt; aImageData: TWasmPointer) : TCanvasError;
+    function SetLineCap(const aID : TOffscreenCanvasID; aCap: TCanvasLinecap): TCanvasError;
+    function SetLineJoin(const aID : TOffscreenCanvasID; aJoin: TCanvasLineJoin): TCanvasError;
+    function SetLineMiterLimit(const aID : TOffscreenCanvasID; aWidth: TCanvasLineMiterLimit): TCanvasError;
+    function SetLineDash(const aID : TOffscreenCanvasID; aOffset : TFresnelFloat; aPatternCount : LongInt; aPattern : PFresnelFloat): TCanvasError;
+    function SetLineWidth(const aID : TOffscreenCanvasID; aWidth: TCanvasLineWidth): TCanvasError;
+    function SetTextBaseLine(const aID : TOffscreenCanvasID; aBaseLine: TCanvasTextBaseLine): TCanvasError;
+    function SetStrokeStyle(const aID : TOffscreenCanvasID; aRed,aGreen,aBlue,aAlpha: TCanvasColorComponent): TCanvasError;
+    function DrawImage(const aID : TOffscreenCanvasID; aX, aY, aWidth, aHeight: TFresnelFloat; aImageWidth, aImageHeight: LongInt; aImageData: TWasmPointer) : TCanvasError;
+    function DrawImageEx(const aID : TOffscreenCanvasID; DrawData : PFresnelFloat; aImageData: TWasmPointer): TCanvasError;
+
+    function DrawImageObject(const aID : TOffscreenCanvasID; DrawData : PFresnelFloat; aObject: TJSObject; aOpacity : TFresnelFloat): TCanvasError;
+    function DrawImageFromCanvas(const aID : TOffscreenCanvasID; DrawData : PFresnelFloat; const aSource: TOffscreenCanvasID; aOpacity : TFresnelFloat): TCanvasError;
+    function DrawImageFromImageBitmap(const aID : TOffscreenCanvasID; DrawData : PFresnelFloat; aImageBitmapID : Integer; aOpacity : TFresnelFloat): TCanvasError;
+    function ReleaseImageBitmap(aImageBitmapID : Integer): TCanvasError;
+    function GetImageBitmapSize(aImageBitmapID : Integer; aSizePtr : TWasmPointer): TCanvasError;
+
+    function SetFont(const aID : TOffscreenCanvasID; aFontName : TWasmPointer; aFontNameLen : integer) : TCanvasError;
+    function MeasureText(const aID : TOffscreenCanvasID; aText : TWasmPointer; aTextLen : integer; aMeasureData : TWasmPointer) : TCanvasError;
+    function SetTextShadowParams (const aID : TOffscreenCanvasID;  aOffsetX, aOffsetY, aRadius : TFresnelFloat;  aRed,aGreen,aBlue,aAlpha : TCanvasColorComponent): TCanvasError;
+
+    function CreatePath2D(aFlags : LongInt; aPathCount : LongInt; aPath : PFresnelFloat) : TJSPath2D;
+    function DrawPath(const aID : TOffscreenCanvasID; aFlags : LongInt; aPathCount : LongInt; aPath : PFresnelFloat) : TCanvasError;
+    function PointInPath(const aID : TOffscreenCanvasID; aX,aY : TFresnelFloat; aPathCount : Integer; aPath : PFresnelFloat; aRes : TWasmPointer): TCanvasError;
+
+    function SetTransform(const aID : TOffscreenCanvasID; m11,m12,m21,m22,m31,m32 : TFresnelFloat) : TCanvasError;
+
+    function SaveState(const aID : TOffscreenCanvasID) : TCanvasError;
+    function RestoreState(const aID : TOffscreenCanvasID) : TCanvasError;
+    function RestoreAndSaveState(const aID : TOffscreenCanvasID) : TCanvasError;
+    function ClipAddRect(const aID : TOffscreenCanvasID; aX,aY,aWidth,aHeight: TFresnelFloat): TCanvasError;
+    function ClipAddPolygon(const aID : TOffscreenCanvasID;
+                            APolygonData : TWasmPointer; APolygonCount : Integer;
+                            AClipQuadData : TWasmPointer; ANbClipQuads : Integer): TCanvasError;
+
+    // Timer
+
+    function AllocateTimer(ainterval : LongInt; userdata: TWasmPointer) : TTimerID;
+    procedure DeallocateTimer(timerid: TTimerID);
+
+    // Events
+
+    function GetEvent(aID: TWasmPointer; aMsg: TWasmPointer; Data : TWasmPointer): TCanvasError;
+    function GetEventCount(aCount: TWasmPointer): TCanvasError;
+
+    // Debug
+
+    procedure ConsoleLog(aText : TWasmPointer; aTextLen : integer);
+
+    // UserMedia
+
+    function GetEnumeratedUserMedia(aUTF16SizePtr, aDataUTF16 : TWasmPointer) : TCanvasError;
+    // Use word-sized colors
+    property UseWordColors : Boolean Read FUseWordColors Write FUseWordColors;
+  end;
+
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+implementation
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+
+{ TFresnelHelper }
+
+// RGBAToHTMLMColor
+//
+class function TFresnelHelper.RGBAToHTMLColor(aRed, aGreen, aBlue, aAlpha: Integer): string;
+begin
+  Result := '#' + TJSNumber(JSValue((aRed shl 16) or (aGreen shl 8) or aBlue)).toString(16).padStart(6, '0');
+  if aAlpha < 255 then
+    Result += TJSNumber(JSValue(aAlpha)).toString(16).padStart(2, '0');
+end;
+
+// EncodeKeyboardShiftState
+//
+class function TFresnelHelper.EncodeKeyboardShiftState(keyEvent: TJSObject): LongInt;
+begin
+  Result := 0;
+  if keyEvent['shiftKey'] then Result += WASM_KEYSTATE_SHIFT;
+  if keyEvent['ctrlKey']  then Result += WASM_KEYSTATE_CTRL;
+  if keyEvent['altKey']   then Result += WASM_KEYSTATE_ALT;
+  if keyEvent['metaKey']  then Result += WASM_KEYSTATE_META;
+end;
+
+// FresnelColorToHTMLColor (components)
+//
+class function TFresnelHelper.FresnelColorToHTMLColor(aRed,aGreen,aBlue,aAlpha: TCanvasColorComponent): string;
+begin
+  Result:='rgb('+IntTostr(aRed shr 8)+' '+IntToStr(aGreen shr 8)+' '+inttoStr(aBlue shr 8);
+  if aAlpha<>$FFFF then
+    Result:=Result+' / '+floatToStr(aAlpha/$FFFF);
+  Result:=Result+')';
+end;
+
+// FresnelColorToHTMLColor (TCanvasColor)
+//
+class function TFresnelHelper.FresnelColorToHTMLColor(aColor: TCanvasColor): string;
+const
+  cHex = '0123456789ABCDEF';
+var
+  I : Integer;
+begin
+  Result:='#';
+  aColor:=aColor shr 8;
+  for I:=1 to 6 do
+  begin
+    Result:=Result+cHex[(aColor and $F)+1];
+    aColor:=aColor shr 4;
+  end;
+end;
+
+{ TWindowEvent }
+
+class function TWindowEvent.Create(const aWindowID: TWindowCanvasID; aMsg: TWindowMessageID): TWindowEvent;
+begin
+  Result := TWasmFresnelSharedApi.NewEvent(aWindowID, aMsg);
+end;
+
+{ TOffscreenCanvasReference }
+
+// Create
+//
+constructor TOffscreenCanvasReference.Create(
+  const aID: TOffscreenCanvasID;
+  aAPI: TWasmFresnelSharedApi;
+  aWidth, aHeight: LongInt;
+  aScale : TFresnelFloat
+  );
+begin
+  API := aAPI;
+  OffscreenCanvasID := aID;
+  Canvas := TJSHTMLOffscreenCanvas.New(Round(aWidth * aScale), Round(aHeight * aScale));
+  CanvasContext := Canvas.getContextAs2DContext('2d');
+  Scale := aScale;
+  FontOffsets := TJSMap.new;
+end;
+
+// Resize
+//
+procedure TOffscreenCanvasReference.Resize(aWidth, aHeight: Longint; aScale: TFresnelFloat);
+begin
+  Canvas.width := Round(aWidth * aScale);
+  Canvas.height := Round(aHeight * aScale);
+  Scale := aScale;
+end;
+
+// RemoveCanvas
+//
+procedure TOffscreenCanvasReference.RemoveCanvas;
+begin
+  // Offscreen canvas are GC'ed
+  Canvas := nil;
+  CanvasContext := nil;
+end;
+
+{ TWasmFresnelSharedApi }
+
+// TWasmFresnelSharedApi
+//
+constructor TWasmFresnelSharedApi.Create(aEnv: TPas2JSWASIEnvironment);
+begin
+  inherited Create(aEnv);
+
+  FOffscreenCanvases := TJSMap.New();
+  FGradients := TJSMap.New();
+  FImageBitmaps := TJSMap.New();
+
+  FLogAPICalls := True;
+end;
+
+// LogCall (str)
+//
+procedure TWasmFresnelSharedApi.LogCall(const Msg: String);
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    Writeln(Msg);
+  {$ENDIF}
+end;
+
+// LogCall (fmt, ...)
+//
+procedure TWasmFresnelSharedApi.LogCall(const Fmt: String; const Args: array of const);
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    Writeln(Format(Fmt,Args));
+  {$ENDIF}
+end;
+
+// ExecuteFunctionCall
+//
+function TWasmFresnelSharedApi.ExecuteFunctionCall(const funcName: string; const args: TJSValueDynArray): TCanvasError;
+var
+  callFunction : TJSFunction;
+  resultValue : JSValue;
+begin
+  callFunction := TJSFunction(FImportObject[funcName]);
+
+  if JSValue(callFunction) then
+    begin
+    resultValue := callFunction.apply(TJSObject(Self), args);
+    if isNumber(resultvalue) then
+      result:=Integer(resultValue)
+    else
+      result:=0;
+    end
+  else
+    raise Exception.CreateFmt('Unsupported function "%s"', [ funcName ]);
+end;
+
+// MainThreadWake
+//
+procedure TWasmFresnelSharedApi.MainThreadWake;
+var
+  callback : JSValue;
+begin
+  // if we reach here, we're in the appropriate thread, wake up the wasm
+  callback := InstanceExports['__fresnel_main_thread_wake'];
+  if callback then
+    TProcedure(callback)();
+end;
+
+// GetOffscreenCanvasRef
+//
+function TWasmFresnelSharedApi.GetOffscreenCanvasRef(const aID: TOffscreenCanvasID): TOffscreenCanvasReference;
+var
+  jsRef : JSValue;
+  resultRef : TOffscreenCanvasReference absolute jsRef;
+begin
+  jsRef := FOffscreenCanvases.get(aID);
+  if jsRef  then
+    Result := resultRef
+  else
+    Result := nil;
+end;
+
+// GetOffscreenContext2D
+//
+function TWasmFresnelSharedApi.GetOffscreenContext2D(const aID: TOffscreenCanvasID): TJSOffscreenCanvasRenderingContext2D;
+var
+  Ref : TOffscreenCanvasReference;
+begin
+  Ref := GetOffscreenCanvasRef(aID);
+  if Assigned(Ref) then
+    Result := Ref.CanvasContext
+  else
+  begin
+    Console.Warn('Fresnel: Unknown canvas : ', aID.ToString);
+    Result := nil;
+  end;
+end;
+
+// StoreImageBitmap
+//
+function TWasmFresnelSharedApi.StoreImageBitmap(const aImageBitmap: TJSImageBitmap): Integer;
+begin
+  Inc(vLastImageBitmapID);
+  Result := vLastImageBitmapID;
+  FImageBitmaps.&set(Result, aImageBitmap);
+end;
+
+// DeleteImageBitmap
+//
+function TWasmFresnelSharedApi.DeleteImageBitmap(const aID: Integer) : Boolean;
+var
+  lImageBitmap : TJSImageBitmap;
+begin
+  lImageBitmap := TJSImageBitmap(FImageBitmaps.get(aID));
+  if JSValue(lImageBitmap) then
+  begin
+    FImageBitmaps.delete(aID);
+    lImageBitmap.close();
+    Result := True;
+  end else Result := False;
+end;
+
+// GetImageBitmap
+//
+function TWasmFresnelSharedApi.GetImageBitmap(const aID: Integer): TJSImageBitmap;
+begin
+  Result := TJSImageBitmap(FImageBitmaps.get(aID));
+  if not JSValue(Result) then
+    Result := nil;
+end;
+
+// SetCreateDefaultCanvas
+//
+procedure TWasmFresnelSharedApi.SetCreateDefaultCanvas(AValue: Boolean);
+var
+  defaultCanvasID : TOffscreenCanvasID;
+begin
+  if FCreateDefaultCanvas = AValue then Exit;
+
+  FCreateDefaultCanvas := AValue;
+  if FCreateDefaultCanvas and (DefaultCanvas = nil) then
+  begin
+    // TODO: merge with AllocateOffscreenCanvas
+    Inc(vLastOffscreenCanvasID);
+    defaultCanvasID := vLastOffscreenCanvasID;
+    FDefaultCanvas := TOffscreenCanvasReference.Create(defaultCanvasID, Self, 128, 64, 1);
+    FOffscreenCanvases.&set(defaultCanvasID, FDefaultCanvas);
+  end;
+end;
+
+// EnqueueEvent
+//
+procedure TWasmFresnelSharedApi.EnqueueEvent(aEvent: TWindowEvent);
+begin
+  Inc(FEventCount);
+  if FEventQueueHead = nil then
+  begin
+    FEventQueueHead := aEvent;
+    FEventQueueTail := aEvent;
+  end
+  else
+  begin
+    FEventQueueTail.NextEvent := aEvent;
+    FEventQueueTail := aEvent;
+  end;
+  ProcessMessages;
+end;
+
+// DequeueEvent
+//
+function TWasmFresnelSharedApi.DequeueEvent: TWindowEvent;
+begin
+  if FEventQueueHead = nil then Exit(nil);
+
+  Dec(FEventCount);
+  Result := FEventQueueHead;
+  FEventQueueHead := Result.NextEvent;
+  Result.NextEvent := nil;
+end;
+
+// NewEvent
+//
+class function TWasmFresnelSharedApi.NewEvent(aWindowID: TWindowCanvasID; aMessageID: TWindowMessageID): TWindowEvent;
+begin
+  if vEventRecycler = nil then
+  begin
+    Result := TWindowEvent(TJSObject.new);
+    Result.NextEvent := nil;
+  end
+  else
+  begin
+    Result := vEventRecycler;
+    vEventRecycler := Result.NextEvent;
+    Result.NextEvent := nil;
+  end;
+  Result.WindowID := aWindowID;
+  Result.Msg := aMessageID;
+end;
+
+// RecycleEvent
+//
+class procedure TWasmFresnelSharedApi.RecycleEvent(aEvent: TWindowEvent);
+begin
+  aEvent.NextEvent := vEventRecycler;
+  vEventRecycler := aEvent;
+  aEvent.Param0 := 0;
+  aEvent.Param1 := 0;
+  aEvent.Param2 := 0;
+  aEvent.Param3 := 0;
+end;
+
+// ProcessMessages
+//
+
+procedure TWasmFresnelSharedApi.ProcessMessages;
+
+  procedure Prepare;
+  begin
+    if not Assigned(InstanceExports) then
+      Console.Error('No instance exports !')
+    else
+    begin
+      FProcessMessagesCallback := InstanceExports['__fresnel_process_message'];
+      if not FProcessMessagesCallback then
+        Console.Error('No processmessages callback !');
+    end;
+  end;
+
+var
+  newTick : NativeInt;
+begin
+  if not FProcessMessagesCallback then
+    Prepare;
+
+  newTick := TJSDate.now;
+  TProcessMessagesCallback(FProcessMessagesCallback)(FLastTick, newTick);
+  FLastTick := newTick;
+end;
+
+// FillImportObject
+//
+procedure TWasmFresnelSharedApi.FillImportObject(aObject: TJSObject);
+begin
+  FImportObject := aObject;
+
+  // Canvas
+
+  aObject['canvas_allocate_offscreen'] := @AllocateOffscreenCanvas;
+  aObject['canvas_deallocate_offscreen'] := @DeAllocateOffscreenCanvas;
+  aObject['canvas_resize_offscreen'] := @ResizeOffscreenCanvas;
+
+  aObject['canvas_moveto'] := @Moveto;
+  aObject['canvas_lineto'] := @LineTo;
+  aObject['canvas_stroke'] := @Stroke;
+  aObject['canvas_beginpath'] := @BeginPath;
+  aObject['canvas_arc'] := @Arc;
+  aObject['canvas_fillrect'] := @fillrect;
+  aObject['canvas_strokerect'] := @strokerect;
+  aObject['canvas_clearrect'] := @ClearRect;
+  aObject['canvas_stroketext'] := @StrokeText;
+  aObject['canvas_filltext'] := @FillText;
+  aObject['canvas_set_fillstyle'] := @SetFillStyle;
+  aObject['canvas_set_fillstyle_string'] := @SetFillStyleString;
+  aObject['canvas_linear_gradient_fillstyle'] := @SetLinearGradientFillStyle;
+  aObject['canvas_image_fillstyle'] := @SetImageFillStyle;
+  aObject['canvas_set_strokestyle'] := @SetStrokeStyle;
+  aObject['canvas_set_linewidth'] := @SetLineWidth;
+  aObject['canvas_set_linecap'] := @SetLineCap;
+  aObject['canvas_set_linejoin'] := @SetLineJoin;
+  aObject['canvas_set_linemiterlimit'] := @SetLineMiterLimit;
+  aObject['canvas_set_linedash'] := @SetLineDash;
+  aObject['canvas_set_textbaseline'] := @SetTextBaseLine;
+  aObject['canvas_draw_image'] := @DrawImage;
+  aObject['canvas_draw_image_ex'] := @DrawImageEx;
+  aObject['canvas_draw_image_from_canvas'] := @DrawImageFromCanvas;
+  aObject['canvas_draw_imagebitmap'] := @DrawImageFromImageBitmap;
+  aObject['canvas_release_imagebitmap'] := @ReleaseImageBitmap;
+  aObject['canvas_imagebitmap_getsize'] := @GetImageBitmapSize;
+  aObject['canvas_set_font'] := @SetFont;
+  aObject['canvas_measure_text'] := @MeasureText;
+  aObject['canvas_set_textshadow_params'] := @SetTextShadowParams;
+  aObject['canvas_roundrect'] := @RoundRect;
+  aObject['canvas_draw_path'] := @DrawPath;
+  aObject['canvas_point_in_path'] := @PointInPath;
+  aObject['canvas_set_transform'] := @SetTransForm;
+  aObject['canvas_save_state'] := @SaveState;
+  aObject['canvas_restore_state'] := @RestoreState;
+  aObject['canvas_restore_and_save_state'] := @RestoreAndSaveState;
+  aObject['canvas_clip_add_rect'] := @ClipAddRect;
+  aObject['canvas_clip_add_polygon'] := @ClipAddPolygon;
+
+  // Timer
+  aObject['timer_allocate'] := @AllocateTimer;
+  aObject['timer_deallocate'] := @DeAllocateTimer;
+
+  // Event
+  aObject['event_get'] := @GetEvent;
+  aObject['event_count'] := @GetEventCount;
+
+  // Debug
+  aObject['console_log'] := @ConsoleLog;
+end;
+
+// ImportName
+//
+function TWasmFresnelSharedApi.ImportName: String;
+begin
+  Result:='fresnel_api';
+end;
+
+// DrawBaseLine
+//
+procedure TWasmFresnelSharedApi.DrawBaseLine(C: TJSOffscreenCanvasRenderingContext2D; S: String; X, Y: Double);
+var
+  M : TJSTextMetrics;
+  Style : JSValue;
+begin
+  if daText in DebugApis then
+  begin
+    M:=C.measureText(S);
+    Style:=C.StrokeStyle;
+    C.StrokeStyle:='rgb(255 0 0 /1)';
+    C.beginPath;
+    C.moveTo(X,Y);
+    C.lineTo(X+M.width,y);
+    C.stroke;
+    C.StrokeStyle:=Style;
+  end;
+end;
+
+// DrawClipRect
+//
+procedure TWasmFresnelSharedApi.DrawClipRect(aRef: TOffscreenCanvasReference; aX, aY, aWidth, aHeight: double);
+var
+  context : TJSOffscreenCanvasRenderingContext2D;
+begin
+  context := aRef.CanvasContext;
+  context.save;
+  context.strokeStyle := 'rgb(255 0 0)';
+  context.lineWidth := 1;
+  context.strokeRect(aX-1, aY-1, aWidth+2, aHeight+2);
+  context.restore;
+end;
+
+// AllocateOffscreenCanvas
+//
+function TWasmFresnelSharedApi.AllocateOffscreenCanvas(
+  aSizeX, aSizeY: LongInt;
+  aScale : TFresnelFloat;
+  aBitmap: TWasmPointer;
+  aID: TWasmPointer
+  ) : TCanvasError;
+var
+  canvasRef : TOffscreenCanvasReference;
+
+  procedure PutBitmapOnCanvas;
+  var
+    dataArray : TJSUint8ClampedArray;
+    imgData : TJSImageData;
+    imgCanvas : TJSHTMLOffscreenCanvas;
+    scaledSizeX, scaledSizeY : Integer;
+  begin
+    dataArray := TJSUint8ClampedArray.New(MemoryDataView.buffer, aBitmap, aSizeX*aSizeY*4);
+    imgData := TJSImageData.new(TJSUint8ClampedArray(dataArray.slice), aSizeX, aSizeY);
+
+    scaledSizeX := canvasRef.Canvas.width;
+    scaledSizeY := canvasRef.Canvas.height;
+    if (scaledSizeX <> aSizeX) or (scaledSizeY <> aSizeY) then
+    begin
+      imgCanvas := TJSHTMLOffscreenCanvas.New(scaledSizeX, scaledSizeY);
+      imgCanvas.getContextAs2DContext('2d').putImageData(imgData, 0, 0);
+      canvasRef.CanvasContext.drawImage(imgCanvas, 0, 0, scaledSizeX, scaledSizeY);
+    end
+    else
+    begin
+      canvasRef.CanvasContext.putImageData(imgData, 0, 0);
+    end;
+  end;
+
+var
+  canvasID : TOffscreenCanvasID;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.AllocateOffScreenCanvas(%d,%d,[%x])', [aSizeX, aSizeY, aBitMap]);
+  {$ENDIF}
+
+  WebAssemblyMemory := Env.Memory.buffer;
+
+  Inc(vLastOffscreenCanvasID);
+  canvasID := vLastOffscreenCanvasID;
+
+  canvasRef := TOffscreenCanvasReference.Create(canvasID, Self, aSizeX, aSizeY, aScale);
+  FOffscreenCanvases.&set(canvasID, canvasRef);
+  MemoryDataView.setUint32(aID, canvasID, env.IsLittleEndian);
+
+  if aBitmap <> 0 then
+    PutBitmapOnCanvas;
+
+  canvasRef.CanvasContext.scale(aScale, aScale);
+
+  Result := ECANVAS_SUCCESS;
+
+  WebAssemblyMemory := nil;
+end;
+
+// DeAllocateOffscreenCanvas
+//
+function TWasmFresnelSharedApi.DeAllocateOffscreenCanvas(const aID: TOffscreenCanvasID): TCanvasError;
+var
+  lCanvasRef : TOffscreenCanvasReference;
+begin
+  lCanvasRef := GetOffscreenCanvasRef(aID);
+  if lCanvasRef = nil then
+    Exit(ECANVAS_NOCANVAS);
+  FOffscreenCanvases.delete(aID);
+  lCanvasRef.Free;
+  Result := ECANVAS_SUCCESS;
+end;
+
+// ResizeOffscreenCanvas
+//
+function TWasmFresnelSharedApi.ResizeOffscreenCanvas(const aID: TOffscreenCanvasID; aSizeX, aSizeY: Longint; aScale: TFresnelFloat): TCanvasError;
+var
+  lCanvasRef : TOffscreenCanvasReference;
+begin
+  lCanvasRef := GetOffscreenCanvasRef(aID);
+  if lCanvasRef = nil then
+    Exit(ECANVAS_NOCANVAS);
+  lCanvasRef.Resize(aSizeX, aSizeY, aScale);
+  // resizing canvas resets states
+  lCanvasRef.CanvasContext.scale(aScale, aScale);
+  Result := ECANVAS_SUCCESS;
+end;
+
+// MoveTo
+//
+function TWasmFresnelSharedApi.MoveTo(const aID: TOffscreenCanvasID; aX, aY: TFresnelFloat): TCanvasError;
+var
+  C : TJSOffscreenCanvasRenderingContext2D;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.MoveTo(%d,%g,%g)',[aID,aX,aY]);
+  {$ENDIF}
+  Result := ECANVAS_NOCANVAS;
+  C := GetOffscreenContext2D(aID);
+  if Assigned(C) then
+  begin
+    C.moveTo(aX, aY);
+    Result := ECANVAS_SUCCESS;
+  end;
+end;
+
+// LineTo
+//
+function TWasmFresnelSharedApi.LineTo(const aID: TOffscreenCanvasID; aX, aY: TFresnelFloat): TCanvasError;
+var
+  C : TJSOffscreenCanvasRenderingContext2D;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.LineTo(%d,%g,%g)',[aID,aX,aY]);
+  {$ENDIF}
+  Result:=ECANVAS_NOCANVAS;
+  C := GetOffscreenContext2D(aID);
+  if Assigned(C) then
+  begin
+    C.lineto(aX, aY);
+    Result:=ECANVAS_SUCCESS;
+  end;
+end;
+
+// Strokg
+//
+function TWasmFresnelSharedApi.Stroke(const aID: TOffscreenCanvasID): TCanvasError;
+var
+  C : TJSOffscreenCanvasRenderingContext2D;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.Stroke(%d)',[aID]);
+  {$ENDIF}
+  C := GetOffscreenContext2D(aID);
+  if not Assigned(C) then
+    Exit(ECANVAS_NOCANVAS);
+  C.Stroke;
+  Result := ECANVAS_SUCCESS;
+end;
+
+// BeginPath
+//
+function TWasmFresnelSharedApi.BeginPath(const aID: TOffscreenCanvasID): TCanvasError;
+var
+  C : TJSOffscreenCanvasRenderingContext2D;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  If LogAPICalls then
+    LogCall('Canvas.BeginPath(%d)',[aID]);
+  {$ENDIF}
+  C := GetOffscreenContext2D(aID);
+  if not Assigned(C) then
+    Exit(ECANVAS_NOCANVAS);
+  C.beginPath;
+  Result:=ECANVAS_SUCCESS;
+end;
+
+// Arc
+//
+function TWasmFresnelSharedApi.Arc(
+    const aID: TOffscreenCanvasID;
+    aX, aY, aRadiusX, aRadiusY, aStartAngle, aEndAngle, aRotate: TFresnelFloat;
+    aFlags: LongInt
+    ): TCanvasError;
+var
+  lCtx : TJSOffscreenCanvasRenderingContext2D;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.Arc(%d,%g,%g,%g,%g,%g,%g)',[aID,X,Y,RadiusX,RadiusY,StartAngle,EndAngle]);
+  {$ENDIF}
+  lCtx := GetOffscreenContext2D(aID);
+  if lCtx = nil then
+    Exit(ECANVAS_NOCANVAS);
+
+  lCtx.beginPath;
+  // Arc is about 4x faster than Ellipse on Chromium as of march 2025
+  if aRadiusX = aRadiusY then
+    lCtx.Arc(aX, aY, aRadiusX, aStartangle, aEndAngle)
+  else
+    lCtx.Ellipse(aX, aY, aRadiusX, aRadiusY, aRotate, aStartangle, aEndAngle);
+  if (aFlags and ARC_FILL) <> 0 then
+    lCtx.fill()
+  else lCtx.stroke();
+
+  Result := ECANVAS_SUCCESS;
+end;
+
+// FillRect
+//
+function TWasmFresnelSharedApi.FillRect(const aID: TOffscreenCanvasID; X, Y, Width,
+ Height: TFresnelFloat): TCanvasError;
+var
+  C : TJSOffscreenCanvasRenderingContext2D;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.FillRect(%d,%g,%g,%g,%g)',[aID,X,Y,Width,Height]);
+  {$ENDIF}
+  C := GetOffscreenContext2D(aID);
+  if not Assigned(C) then
+    Exit(ECANVAS_NOCANVAS);
+  C.FillRect(x, y, width, height);
+  Result := ECANVAS_SUCCESS;
+end;
+
+// StrokeRect
+//
+function TWasmFresnelSharedApi.StrokeRect(const aID: TOffscreenCanvasID;
+  X, Y, Width, Height: TFresnelFloat): TCanvasError;
+var
+  C : TJSOffscreenCanvasRenderingContext2D;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.StrokeRect(%d,%g,%g,%g,%g)',[aID,X,Y,Width,Height]);
+  {$ENDIF}
+  C := GetOffscreenContext2D(aID);
+  if not Assigned(C) then
+    Exit(ECANVAS_NOCANVAS);
+  C.StrokeRect(X,Y,Width,Height);
+  Result := ECANVAS_SUCCESS;
+end;
+
+// ClearRect
+//
+function TWasmFresnelSharedApi.ClearRect(const aID: TOffscreenCanvasID; X, Y, Width,
+ Height: TFresnelFloat): TCanvasError;
+var
+  C : TJSOffscreenCanvasRenderingContext2D;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.ClearRect(%d,%g,%g,%g,%g)',[aID,X,Y,Width,Height]);
+  {$ENDIF}
+  C := GetOffscreenContext2D(aID);
+  if not Assigned(C) then
+    Exit(ECANVAS_NOCANVAS);
+  C.ClearRect(X,Y,Width,Height);
+  Result := ECANVAS_SUCCESS;
+end;
+
+// RoundRect
+//
+function TWasmFresnelSharedApi.RoundRect(const aID: TOffscreenCanvasID;
+ Flags: LongInt; Data: PFresnelFloat): TCanvasError;
+Var
+  C : TJSOffscreenCanvasRenderingContext2D;
+  V : TJSDataView;
+  X,Y,W,H : TFresnelFloat;
+  Radii : TJSArray;
+  Fill : Boolean;
+
+  function GetElement(aOffset : LongInt) : TFresnelFloat;
+  begin
+    Result:=V.getFloat32(Data+(aOffset*SizeFloat32),Env.IsLittleEndian);
+  end;
+
+  procedure AddRadius(aRX,aRY : Double);
+  begin
+    Radii.Push(New(['x',aRX,'y',aRY]))
+  end;
+
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  If LogAPICalls then
+    LogCall('Canvas.RoundRect(%d,%d,[%d])',[aID,Flags,Data]);
+  {$ENDIF}
+  C := GetOffscreenContext2D(aID);
+  if not Assigned(C) then
+    Exit(ECANVAS_NOCANVAS);
+  V:=MemoryDataView;
+  X:=GetElement(ROUNDRECT_BOXTOPLEFTX);
+  Y:=GetElement(ROUNDRECT_BOXTOPLEFTY);
+  W:=GetElement(ROUNDRECT_BOXBOTTOMRIGHTX)-X;
+  H:=GetElement(ROUNDRECT_BOXBOTTOMRIGHTY)-Y;
+  Fill:=(Flags and ROUNDRECT_FLAG_FILL)<>0;
+  Radii:=TJSArray.New;
+  AddRadius(GetElement(ROUNDRECT_RADIITOPLEFTX),GetElement(ROUNDRECT_RADIITOPLEFTY));
+  AddRadius(GetElement(ROUNDRECT_RADIITOPRIGHTX),GetElement(ROUNDRECT_RADIITOPRIGHTY));
+  AddRadius(GetElement(ROUNDRECT_RADIIBOTTOMRIGHTX),GetElement(ROUNDRECT_RADIIBOTTOMRIGHTY));
+  AddRadius(GetElement(ROUNDRECT_RADIIBOTTOMLEFTX),GetElement(ROUNDRECT_RADIIBOTTOMLEFTY));
+  C.BeginPath;
+  C.roundRect(X,Y,W,H,Radii);
+  if Fill then
+    C.fill;
+  C.stroke();
+  Result:=ECANVAS_SUCCESS;
+end;
+
+// StrokeText
+//
+function TWasmFresnelSharedApi.StrokeText(const aID: TOffscreenCanvasID; X,
+ Y: TFresnelFloat; aText: TWasmPointer; aTextLen: LongInt): TCanvasError;
+var
+  C : TJSOffscreenCanvasRenderingContext2D;
+  S : String;
+begin
+  S := GetUTF16FromMem(aText, aTextLen);
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.StrokeText(%d,(%g,%g),''%s'')',[aID,X,Y,S]);
+  {$ENDIF}
+  C := GetOffscreenContext2D(aID);
+  if not Assigned(C) then
+    Exit(ECANVAS_NOCANVAS);
+  C.StrokeText(S,X,Y);
+  if daText in DebugApis then
+    DrawBaseLine(C,S,X,Y);
+  Result:=ECANVAS_SUCCESS;
+end;
+
+// FillText
+//
+function TWasmFresnelSharedApi.FillText(
+  const aID: TOffscreenCanvasID;
+  aX, aY: TFresnelFloat;
+  aText: TWasmPointer; aTextLen: LongInt;
+  aOpacity : TFresnelFloat
+  ): TCanvasError;
+var
+  lCanvas : TOffscreenCanvasReference;
+  lContext : TJSOffscreenCanvasRenderingContext2D;
+  lText : String;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.FillText(%d,(%g,%g),''%s'')', [ aID, aX, Y, lText ]);
+  {$ENDIF}
+  lCanvas := GetOffscreenCanvasRef(aID);
+  if lCanvas = nil then
+    Exit(ECANVAS_NOCANVAS);
+
+  lContext := lCanvas.CanvasContext;
+
+  lText := GetUTF16FromMem(aText, aTextLen);
+  if aOpacity <> 1 then
+    lContext.globalAlpha := aOpacity;
+
+  aY += lCanvas.FontOffset;
+
+  lContext.FillText(lText, aX, aY);
+  if daText in DebugApis then
+    DrawBaseLine(lContext, lText, aX, aY);
+  if aOpacity <> 1 then
+    lContext.globalAlpha := 1;
+  Result := ECANVAS_SUCCESS;
+end;
+
+// SetFillStyle
+//
+function TWasmFresnelSharedApi.SetFillStyle(const aID : TOffscreenCanvasID; aRed, aGreen, aBlue, aAlpha: TCanvasColorComponent): TCanvasError;
+var
+  lContext : TJSOffscreenCanvasRenderingContext2D;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SetFillStyle(%d,%d,%d,%d,%d)',[aID,aRed,aGreen,aBlue,aAlpha]);
+  {$ENDIF}
+  lContext := GetOffscreenContext2D(aID);
+  if lContext = nil then
+    Exit(ECANVAS_NOCANVAS);
+  if UseWordColors then
+    lContext.fillStyle := TFresnelHelper.FresnelColorToHTMLColor(aRed, aGreen, aBlue, aAlpha)
+  else
+    lContext.fillStyle := TFresnelHelper.RGBAToHTMLColor(aRed, aGreen, aBlue, aAlpha);
+  Result := ECANVAS_SUCCESS;
+end;
+
+// SetFillStyleString
+//
+function TWasmFresnelSharedApi.SetFillStyleString(
+  const aID: TOffscreenCanvasID;
+  aTextPtr: TWasmPointer; aTextLen: LongInt
+  ): TCanvasError;
+var
+  lCanvasRef : TOffscreenCanvasReference;
+  lContext : TJSOffscreenCanvasRenderingContext2D;
+  lStyle : String;
+
+  procedure SetGradientFillStyle;
+  const
+    cMAX_GRADIENTS = 32; // completely arbitrary max number of gradients to keep around
+  var
+    gradient : JSValue;
+    parameters : TJSValueDynArray;
+    p, i : Integer;
+  begin
+    gradient := FGradients.get(lStyle);
+    if not gradient then
+    begin
+      if FGradients.size > cMAX_GRADIENTS then
+        FGradients.clear;
+      p := TJSString(lStyle).indexOf(' ');
+      if TJSString(lStyle).startsWith('gradient.linear') then
+      begin
+        parameters := TJSValueDynArray(TJSJSON.parse('[' + TJSString(lStyle).slice(p+1) + ']'));
+        //gradient := TJSFunction(@lContext.createLinearGradient).apply(...);
+        gradient := TJSFunction(TJSObject(lContext)['createLinearGradient']).apply(
+          lContext, TJSValueDynArray(parameters[0]));
+        for i := 1 to High(parameters) do
+        begin
+          TJSFunction(TJSObject(gradient)['addColorStop']).apply(TJSObject(gradient), TJSValueDynArray(parameters[i]));
+        end;
+        FGradients.&set(lStyle, gradient);
+      end;
+    end;
+    lContext.fillStyle := gradient;
+  end;
+
+  procedure SetBitmapFillStyle;
+  var
+    parameters : TJSValueDynArray;
+    p : Integer;
+    bitmapID : TOffscreenCanvasID;
+    pattern : TJSCanvasPattern;
+    bitmapRef : TOffscreenCanvasReference;
+    wrapMode : Integer;
+    tempCanvas: TJSHTMLOffscreenCanvas;
+    tempCtx: TJSOffscreenCanvasRenderingContext2D;
+  begin
+    p := TJSString(lStyle).indexOf(' ');
+    parameters := TJSValueDynArray(TJSJSON.parse('[' + TJSString(lStyle).slice(p+1) + ']'));
+    bitmapID := Integer(parameters[0]);
+    wrapMode := Integer(parameters[1]);
+    bitmapRef := GetOffscreenCanvasRef(bitmapID);
+
+    case wrapMode of
+      0: // Tile
+        pattern := lContext.createPattern(bitmapRef.Canvas, 'repeat');
+
+      1: // TileOriginal
+        begin
+          lContext.save;
+          // Reset any transform that might affect the pattern scaling
+          lContext.setTransform(1, 0, 0, 1, 0, 0);
+          pattern := lContext.createPattern(bitmapRef.Canvas, 'repeat');
+          lContext.restore;
+        end;
+
+      2: // TileStretch
+        begin
+          tempCanvas := TJSHTMLOffscreenCanvas.new(lContext.canvas.width, lContext.canvas.height);
+          tempCtx := TJSOffscreenCanvasRenderingContext2D(tempCanvas.getContext('2d'));
+          tempCtx.drawImage(bitmapRef.Canvas, 0, 0, tempCanvas.width, tempCanvas.height);
+          pattern := lContext.createPattern(tempCanvas, 'repeat');
+        end;
+    end;
+
+    lContext.fillStyle := pattern;
+  end;
+
+begin
+  lStyle := GetUTF16FromMem(aTextPtr, aTextLen);
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SetFillStyle(%d,"%s")',[aID,lStyle]);
+  {$ENDIF}
+
+  lCanvasRef := GetOffscreenCanvasRef(aID);
+  if lCanvasRef = nil then
+    Exit(ECANVAS_NOCANVAS);
+  lContext := lCanvasRef.CanvasContext;
+
+  if TJSString(lStyle).startsWith('gradient') then
+    SetGradientFillStyle
+  else if TJSString(lStyle).startsWith('bitmap') then
+    SetBitmapFillStyle
+  else lContext.fillStyle := GetUTF16FromMem(aTextPtr, aTextLen);
+
+  Result := ECANVAS_SUCCESS;
+end;
+
+// SetLinearGradientFillStyle
+//
+function TWasmFresnelSharedApi.SetLinearGradientFillStyle(
+  const aID: TOffscreenCanvasID;
+  aStartX, aStartY, aEndX, aEndY: TFresnelFloat;
+  aColorPointCount: LongInt; aColorPoints: TWasmPointer
+  ) : TCanvasError;
+var
+  i,P : LongInt;
+  Red,Green,Blue,Alpha: LongInt;
+  offset : double;
+  lGradient : TJSCanvasGradient;
+  lContext : TJSOffscreenCanvasRenderingContext2D;
+  lDataView : TJSDataView;
+  lColor : String;
+
+  function GetLongInt: LongInt;
+  begin
+    Result := lDataView.getInt32(P, Env.IsLittleEndian);
+    Inc(P, SizeInt32);
+  end;
+
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SetLinearGradientFillStyle(%d,(%g,%g),(%g,%g),%d,[%x])',[aID,aStartX, aStartY, aEndX, aEndY, aColorPointCount,aColorPoints]);
+  {$ENDIF}
+  lContext := GetOffscreenContext2D(aID);
+  if lContext = nil then
+    Exit(ECANVAS_NOCANVAS);
+  lGradient := lContext.createLinearGradient(aStartX, aStartY, aEndX, aEndY);
+  lDataView := MemoryDataView;
+  P := aColorPoints;
+  for i := 0 to aColorPointCount-1 do
+  begin
+    Red := GetLongInt;
+    Green := GetLongInt;
+    Blue := GetLongInt;
+    Alpha := GetLongInt;
+    offset := GetLongInt/10000;
+    lColor := TFresnelHelper.FresnelColorToHTMLColor(Red, Green, Blue, Alpha);
+    lGradient.addColorStop(offset, lColor);
+  end;
+  lContext.fillStyleAsGradient := lGradient;
+  Exit(ECANVAS_SUCCESS);
+end;
+
+function TWasmFresnelSharedApi.SetImageFillStyle(const aID: TOffscreenCanvasID; Flags: LongInt;
+  aImageWidth, aImageHeight: LongInt; aImageData: TWasmPointer): TCanvasError;
+var
+  OSC : TJSHTMLOffscreenCanvas;
+  ImgData : TJSImageData;
+//  OSCImgBitmap : TJSImageBitmap;
+  Canv,Canv2 : TJSOffscreenCanvasRenderingContext2D;
+  D : TJSUint8ClampedArray;
+  V : TJSDataView;
+  S : String;
+
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SetImageFillStyle(%d,%d,(%d,%d),[%x])',[aID,flags,aImageWidth,aImageHeight,aImageData]);
+  {$ENDIF}
+  Canv:=GetOffscreenContext2D(aID);
+  if Not Assigned(Canv) then
+    Exit(ECANVAS_NOCANVAS);
+  V:=MemoryDataView;
+  D:=TJSUint8ClampedArray.New(V.Buffer,aImageData,aImageWidth*aImageWidth*4);
+  ImgData:=TJSImageData.new(TJSUint8ClampedArray(D.slice), aImageWidth, aImageWidth);
+  OSC:=TJSHTMLOffscreenCanvas.New(aImageWidth,aImageHeight);
+  Canv2:=OSC.getContextAs2DContext('2d');
+  Canv2.ClearRect(0,0,aImageWidth,aImageHeight);
+  Canv2.putImageData(ImgData,0,0);
+  Case flags and 3 of
+    IMAGEFILLSTYLE_NOREPEAT : s:='no-repeat';
+    IMAGEFILLSTYLE_REPEAT   : s:='repeat';
+    IMAGEFILLSTYLE_REPEATX  : s:='repeat-x';
+    IMAGEFILLSTYLE_REPEATY  : s:='repeat-y';
+  end;
+  Canv.fillStyleAsPattern:=Canv.createPattern(OSC,S);
+end;
+
+// SetLineCap
+//
+function TWasmFresnelSharedApi.SetLineCap(const aID: TOffscreenCanvasID; aCap : TCanvasLinecap):  TCanvasError;
+var
+  Canv : TJSOffscreenCanvasRenderingContext2D;
+  S : String;
+begin
+  S:=LineCapToString(aCap);
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SetLineCap(%d,%s)',[aID,S]);
+  {$ENDIF}
+  Canv:=GetOffscreenContext2D(aID);
+  if not Assigned(Canv) then
+    Exit(ECANVAS_NOCANVAS);
+  Canv.lineCap:=S;
+  Result:=ECANVAS_SUCCESS;
+end;
+
+function TWasmFresnelSharedApi.SetLineJoin(const aID: TOffscreenCanvasID; aJoin : TCanvasLineJoin):  TCanvasError;
+
+var
+  Canv:TJSOffscreenCanvasRenderingContext2D;
+  S : String;
+
+begin
+  S:=LineJoinToString(aJoin);
+  {$IFNDEF NOLOGAPICALLS}
+  If LogAPICalls then
+    begin
+    LogCall('Canvas.SetLineJoin(%d,%s)',[aID,S]);
+    end;
+  {$ENDIF}
+  Canv:=GetOffscreenContext2D(aID);
+  if Not Assigned(Canv) then
+    Exit(ECANVAS_NOCANVAS);
+  Canv.lineJoin:=S;
+  Result:=ECANVAS_SUCCESS;
+end;
+
+
+function TWasmFresnelSharedApi.SetLineMiterLimit(const aID: TOffscreenCanvasID; aWidth : TCanvasLineMiterLimit):  TCanvasError;
+
+var
+  Canv:TJSOffscreenCanvasRenderingContext2D;
+
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SetLineMiterLimit(%d,%d)',[aID,aWidth]);
+  {$ENDIF}
+  Canv:=GetOffscreenContext2D(aID);
+  if Not Assigned(Canv) then
+    Exit(ECANVAS_NOCANVAS);
+  Canv.miterLimit:=aWidth;
+  Result:=ECANVAS_SUCCESS;
+  Writeln('Canvas.SetLineMiterLimit not implemented');
+end;
+
+function TWasmFresnelSharedApi.SetLineDash(const aID: TOffscreenCanvasID; aOffset: TFresnelFloat;
+  aPatternCount: LongInt; aPattern: PFresnelFloat): TCanvasError;
+
+var
+  Dashes : TJSArray;
+  V : TJSDataView;
+  I : Integer;
+  P : TWasmPointer;
+  Canv : TJSOffscreenCanvasRenderingContext2D;
+
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  If LogAPICalls then
+    begin
+    LogCall('Canvas.SetLineDash(%d,%g,%d,[%x])',[aID,aOffset,aPatternCount,aPattern]);
+    end;
+  {$ENDIF}
+  Canv:=GetOffscreenContext2D(aID);
+  if Not Assigned(Canv) then
+    Exit(ECANVAS_NOCANVAS);
+  Dashes:=TJSArray.New;
+  if aPatternCount>0 then
+    begin
+    V:=MemoryDataView;
+    P:=aPattern;
+    for I:=0 to APatternCount-1 do
+      begin
+      Dashes.Push(v.GetFloat32(P,env.IsLittleEndian));
+      Inc(P,SizeFloat32);
+      end;
+    end;
+  Canv.lineDashOffset:=aOffset;
+  Canv.setLineDash(Dashes);
+end;
+
+// SetLineWidth
+//
+function TWasmFresnelSharedApi.SetLineWidth(const aID : TOffscreenCanvasID; aWidth : TCanvasLineWidth):  TCanvasError;
+var
+  Canv : TJSOffscreenCanvasRenderingContext2D;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SetLineWidth(%d,%g)',[aID,aWidth]);
+  {$ENDIF}
+  Canv:=GetOffscreenContext2D(aID);
+  if not Assigned(Canv) then
+    Exit(ECANVAS_NOCANVAS);
+  Canv.LineWidth:=aWidth;
+  Result:=ECANVAS_SUCCESS;
+end;
+
+// SetTextBaseLine
+//
+function TWasmFresnelSharedApi.SetTextBaseLine(const aID: TOffscreenCanvasID; aBaseLine: TCanvasTextBaseLine): TCanvasError;
+var
+  Ref :TOffscreenCanvasReference;
+  S : String;
+begin
+  S := TextBaseLineToString(aBaseLine);
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SetTextBaseLine(%d,%s)',[aID,S]);
+  {$ENDIF}
+  Ref := GetOffscreenCanvasRef(aID);
+  if not Assigned(Ref) then
+    Exit(ECANVAS_NOCANVAS);
+  Ref.CanvasContext.TextBaseLine := S;
+  Result := ECANVAS_SUCCESS;
+end;
+
+// SetStrokeStyle
+//
+function TWasmFresnelSharedApi.SetStrokeStyle(const aID: TOffscreenCanvasID; aRed, aGreen, aBlue, aAlpha: TCanvasColorComponent): TCanvasError;
+var
+  Ref : TOffscreenCanvasReference;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SetStrokeStyle(%d,%d,%d,%d,%d)',[aID,aRed,aGreen,aBlue,aAlpha]);
+  {$ENDIF}
+  Ref := GetOffscreenCanvasRef(aID);
+  if not Assigned(Ref) then
+    Exit(ECANVAS_NOCANVAS);
+  if UseWordColors then
+    Ref.CanvasContext.StrokeStyle := TFresnelHelper.FresnelColorToHTMLColor(aRed, aGreen, aBlue, aAlpha)
+  else
+    Ref.CanvasContext.StrokeStyle := TFresnelHelper.RGBAToHTMLColor(aRed, aGreen, aBlue, aAlpha);
+
+  Result := ECANVAS_SUCCESS;
+end;
+
+function TWasmFresnelSharedApi.DrawImage(const aID: TOffscreenCanvasID; aX, aY,
+ aWidth, aHeight: TFresnelFloat; aImageWidth, aImageHeight: LongInt;
+ aImageData: TWasmPointer): TCanvasError;
+
+var
+  V : TJSDataView;
+  D : TJSUint8ClampedArray;
+  ImgData : TJSImageData;
+  Canv : TJSOffscreenCanvasRenderingContext2D;
+
+{$IFDEF IMAGE_USEOSC}
+  Canv2 : TJSOffscreenCanvasRenderingContext2D;
+  OSC : TJSHTMLOffscreenCanvas;
+{$ENDIF}
+
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  If LogAPICalls then
+    begin
+    LogCall('Canvas.DrawImage(%d,(%g,%g),(%gx%g),(%dx%d)',[aID,aX,aY,aWidth,aHeight,aImageWidth,aImageHeight]);
+    end;
+  {$ENDIF}
+  Canv:=GetOffscreenContext2D(aID);
+  if Not Assigned(Canv) then
+    Exit(ECANVAS_NOCANVAS);
+  V:=MemoryDataView;
+  D:=TJSUint8ClampedArray.New(V.Buffer,aImageData,aImageWidth*aImageWidth*4);
+  ImgData:=TJSImageData.new(TJSUint8ClampedArray(D.slice), aImageWidth, aImageWidth);
+{$IFDEF IMAGE_USEOSC}
+  OSC := TJSHTMLOffscreenCanvas.New(aImageWidth,aImageHeight);
+  Canv2:=OSC.getContextAs2DContext('2d');
+  Canv2.ClearRect(0,0,aImageWidth,aImageHeight);
+  Canv2.putImageData(ImgData,0,0);
+  Canv.drawImage(OSC,aX,aY,aWidth,aHeight);
+{$ELSE}
+Window.createImageBitmap(ImgData)._then(
+    function (res : jsvalue) : JSValue
+    var
+      ImgBitmap : TJSImageBitmap absolute res;
+    begin
+      Canv.drawImage(ImgBitmap,aX,aY,aWidth,aHeight);
+    end);
+{$ENDIF}
+  Result:=ECANVAS_SUCCESS;
+end;
+
+function TWasmFresnelSharedApi.DrawImageEx(const aID: TOffscreenCanvasID; DrawData: PFresnelFloat; aImageData: TWasmPointer): TCanvasError;
+
+var
+  V : TJSDataView;
+  D : TJSUint8ClampedArray;
+  ImgData : TJSImageData;
+  Canv : TJSOffscreenCanvasRenderingContext2D;
+  aSrcX,aSrcY,aSrcWidth,aSrcHeight,aDestX,aDestY,aDestWidth,aDestHeight :Double;
+  aImageWidth,aImageHeight : LongInt;
+
+{$IFDEF IMAGE_USEOSC}
+  Canv2 : TJSOffscreenCanvasRenderingContext2D;
+  OSC : TJSHTMLOffscreenCanvas;
+{$ENDIF}
+
+  Function GetD(aIdx : Integer) : LongInt;
+  begin
+    Result:=Round(V.getFloat32(DrawData+aIdx*SizeFloat32,Env.IsLittleEndian));
+  end;
+
+  Function GetS(aIdx : Integer) : Double;
+  begin
+    Result:=v.getFloat32(DrawData+aIdx*SizeFloat32,Env.IsLittleEndian);
+  end;
+
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  If LogAPICalls then
+    LogCall('Canvas.DrawImageEx(%d,[%x],[%x])',[aID,DrawData,aImageData]);
+  {$ENDIF}
+
+  Canv := GetOffscreenContext2D(aID);
+  if not Assigned(Canv) then
+    Exit(ECANVAS_NOCANVAS);
+
+  V:=MemoryDataView;
+  aDestX:=GetS(DRAWIMAGE_DESTX);
+  aDestY:=GetS(DRAWIMAGE_DESTY);
+  aDestWidth:=GetS(DRAWIMAGE_DESTWIDTH);
+  aDestHeight:=GetS(DRAWIMAGE_DESTHEIGHT);
+  aSrcX:=GetS(DRAWIMAGE_SRCX);
+  aSrcY:=GetS(DRAWIMAGE_SRCY);
+  aSrcWidth:=GetS(DRAWIMAGE_SRCWIDTH);
+  aSrcHeight:=GetS(DRAWIMAGE_SRCHEIGHT);
+  aImageWidth:=GetD(DRAWIMAGE_IMAGEWIDTH);
+  aImageHeight:=GetD(DRAWIMAGE_IMAGEHEIGHT);
+  {$IFNDEF NOLOGAPICALLS}
+  If LogAPICalls then
+    begin
+    LogCall('Canvas.DrawImage(%d,[(%g,%g) - (%gx%g)],[(%g,%g) - (%gx%g)],[%dx%d])',[aID,aSrcX,aSrcY,aSrcWidth,aSrcHeight,aDestX,aDestY,aDestWidth,aDestHeight,aImageWidth,aImageWidth]);
+    end;
+  {$ENDIF}
+  D:=TJSUint8ClampedArray.New(V.Buffer,aImageData,aImageWidth*aImageWidth*4);
+  ImgData:=TJSImageData.new(TJSUint8ClampedArray(D.slice), aImageWidth, aImageWidth);
+
+{$IFDEF IMAGE_USEOSC}
+  OSC:=TJSHTMLOffscreenCanvas.New(aImageWidth,aImageHeight);
+  Canv2:=OSC.getContextAs2DContext('2d');
+  Canv2.ClearRect(0,0,aImageWidth,aImageHeight);
+  Canv2.putImageData(ImgData,0,0);
+  Canv.drawImage(OSC,aSrcX,aSrcY,aSrcWidth,aSrcHeight,aDestX,aDestY,aDestWidth,aDestHeight);
+{$ELSE}
+Window.createImageBitmap(ImgData)._then(
+    function (res : jsvalue) : JSValue
+    var
+      ImgBitmap : TJSImageBitmap absolute res;
+    begin
+      Canv.drawImage(ImgBitmap,aX,aY,aWidth,aHeight);
+    end);
+{$ENDIF}
+  Result:=ECANVAS_SUCCESS;
+end;
+
+// DrawImageObject
+//
+function TWasmFresnelSharedApi.DrawImageObject(const aID: TOffscreenCanvasID; DrawData : PFresnelFloat; aObject: TJSObject; aOpacity : TFresnelFloat): TCanvasError;
+var
+  V : TJSDataView;
+  Canv : TJSOffscreenCanvasRenderingContext2D;
+  aSrcX,aSrcY,aSrcWidth,aSrcHeight,aDestX,aDestY,aDestWidth,aDestHeight :Double;
+
+
+  function GetS(aIdx : Integer) : Double;
+  begin
+    Result:=v.getFloat32(DrawData+aIdx*SizeFloat32,Env.IsLittleEndian);
+  end;
+
+begin
+  Canv := GetOffscreenContext2D(aID);
+  if not Assigned(Canv) then
+    Exit(ECANVAS_NOCANVAS);
+
+  V:=MemoryDataView;
+  aDestX:=GetS(DRAWIMAGE_DESTX);
+  aDestY:=GetS(DRAWIMAGE_DESTY);
+  aDestWidth:=GetS(DRAWIMAGE_DESTWIDTH);
+  aDestHeight:=GetS(DRAWIMAGE_DESTHEIGHT);
+  aSrcX:=GetS(DRAWIMAGE_SRCX);
+  aSrcY:=GetS(DRAWIMAGE_SRCY);
+  aSrcWidth:=GetS(DRAWIMAGE_SRCWIDTH);
+  aSrcHeight:=GetS(DRAWIMAGE_SRCHEIGHT);
+
+  if aOpacity < 1 then
+    Canv.GlobalAlpha := aOpacity;
+  Canv.drawImage(aObject, aSrcX, aSrcY, aSrcWidth, aSrcHeight, aDestX, aDestY, aDestWidth, aDestHeight);
+  if aOpacity < 1 then
+    Canv.GlobalAlpha := 1;
+
+  Result:=ECANVAS_SUCCESS;
+end;
+
+// DrawImageFromCanvas
+//
+function TWasmFresnelSharedApi.DrawImageFromCanvas(
+  const aID: TOffscreenCanvasID; DrawData : PFresnelFloat;
+  const aSource: TOffscreenCanvasID; aOpacity : TFresnelFloat
+  ): TCanvasError;
+var
+  LSourceCanvas : TJSOffscreenCanvasRenderingContext2D;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.DrawImageFromCanvas(%d,[%x],[%x])', [ aID, DrawData, aImageData ]);
+  {$ENDIF}
+
+  LSourceCanvas := GetOffscreenContext2D(aSource);
+  if not Assigned(LSourceCanvas) then
+    Exit(ECANVAS_NOCANVAS);
+
+  Result := DrawImageObject(aID, DrawData, LSourceCanvas.canvas, aOpacity);
+end;
+
+// DrawImageFromImageBitmap
+//
+function TWasmFresnelSharedApi.DrawImageFromImageBitmap(const aID: TOffscreenCanvasID; DrawData : PFresnelFloat; aImageBitmapID : Integer; aOpacity : TFresnelFloat): TCanvasError;
+var
+  imageBitmap : TJSImageBitmap;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.DrawImageFromImageBitmap(%d,[%x],%d)', [ aID, DrawData, aImageBitmapID ]);
+  {$ENDIF}
+  imageBitmap := GetImageBitmap(aImageBitmapID);
+  if imageBitmap <> nil then
+    Result := DrawImageObject(aID, DrawData, imageBitmap, aOpacity)
+  else
+  begin
+    writeln('no ImageBitmap of id ', aImageBitmapID);
+    Result := ECANVAS_INVALIDPARAM;
+  end;
+end;
+
+// ReleaseImageBitmap
+//
+function TWasmFresnelSharedApi.ReleaseImageBitmap(aImageBitmapID: Integer): TCanvasError;
+begin
+  if DeleteImageBitmap(aImageBitmapID) then
+    Result := ECANVAS_SUCCESS
+  else Result := ECANVAS_NOIMAGEBITMAP;
+end;
+
+// GetImageBitmapSize
+//
+function TWasmFresnelSharedApi.GetImageBitmapSize(aImageBitmapID: Integer; aSizePtr: TWasmPointer): TCanvasError;
+var
+  imageBitmap : TJSImageBitmap;
+  lView : TJSDataView;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.GetImageBitmapSize(%d,[%x])', [ aImageBitmapID, aSizeFPtr ]);
+  {$ENDIF}
+  imageBitmap := GetImageBitmap(aImageBitmapID);
+  if imageBitmap <> nil then
+  begin
+    lView := MemoryDataView;
+    lView.setInt32(aSizePtr,             imageBitmap.width,  Env.IsLittleEndian);
+    lView.setInt32(aSizePtr + SizeInt32, imageBitmap.height, Env.IsLittleEndian);
+    Result := ECANVAS_SUCCESS;
+  end
+  else Result := ECANVAS_NOIMAGEBITMAP;
+end;
+
+// SetFont
+//
+function TWasmFresnelSharedApi.SetFont(const aID: TOffscreenCanvasID;
+ aFontName: TWasmPointer; aFontNameLen: integer): TCanvasError;
+var
+  lFontName : String;
+  lPreviousFontName, lNewFontName : String;
+  lCanvas : TOffscreenCanvasReference;
+  lContext : TJSOffscreenCanvasRenderingContext2D;
+  lMetrics : TJSTextMetrics;
+begin
+  lFontName := GetUTF16FromMem(aFontName, aFontNameLen);
+
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SetFont(%d,"%s")',[aID,lFontName]);
+  {$ENDIF}
+
+  lCanvas := GetOffscreenCanvasRef(aID);
+  if not Assigned(lCanvas) then
+    Exit(ECANVAS_NOCANVAS);
+
+  lContext := lCanvas.CanvasContext;
+
+  // Note: the browser will normalize the font name
+
+  lPreviousFontName := lContext.font;
+  lContext.font := lFontName;
+  lNewFontName:= lContext.font;
+
+  if lNewFontName <> lPreviousFontName then
+  begin
+    if lCanvas.FontOffsets.has(lNewFontName) then
+    begin
+      lCanvas.FontOffset := TFresnelFloat(lCanvas.FontOffsets.get(lNewFontName));
+    end
+    else
+    begin
+      lMetrics := lContext.measureText('x');
+      lCanvas.FontOffset := (lMetrics.fontBoundingBoxAscent - lMetrics.hangingBaseline) * 0.5;
+      lCanvas.FontOffsets.&set(lNewFontName, lCanvas.FontOffset);
+    end;
+  end;
+
+  lContext.TextBaseLine := 'top';
+
+  Result := ECANVAS_SUCCESS;
+end;
+
+// MeasureText
+//
+function TWasmFresnelSharedApi.MeasureText(const aID: TOffscreenCanvasID;
+ aText: TWasmPointer; aTextLen: integer; aMeasureData: TWasmPointer
+ ): TCanvasError;
+var
+  S : String;
+  lOffscreenRef : TOffscreenCanvasReference;
+  lMetrics : TJSTextMetrics;
+  lView : TJSDataView;
+  H,Asc,Desc : Double;
+  scaleFactor : Double;
+begin
+  S := GetUTF16FromMem(aText, aTextLen);
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.MeasureText(%d,"%s")',[aID,S]);
+{$ENDIF}
+  lOffscreenRef := GetOffscreenCanvasRef(aID);
+  if lOffscreenRef = nil then
+    Exit(ECANVAS_NOCANVAS);
+
+  lMetrics := lOffscreenRef.CanvasContext.measureText(S);
+  Asc := lMetrics.fontBoundingBoxAscent;
+  Desc := lMetrics.fontBoundingBoxDescent;
+  H := Asc + Desc;
+
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.MeasureText(%d,"%s") : [W: %g, H: %g, Asc: %g, Desc: %g]',[aID,S,W,H,Asc,Desc]);
+  {$ENDIF}
+
+  scaleFactor := 1;// / lOffscreenRef.Scale;
+
+  lView := MemoryDataView;
+  lView.setFloat32(
+    aMeasureData + WASMMEASURE_WIDTH*SizeFloat32,
+    lMetrics.width * scaleFactor, env.IsLittleEndian
+  );
+  lView.setFloat32(
+    aMeasureData + WASMMEASURE_HEIGHT*SizeFloat32,
+    H*scaleFactor, env.IsLittleEndian
+  );
+  lView.setFloat32(
+    aMeasureData + WASMMEASURE_ASCENDER*SizeFloat32,
+    Asc*scaleFactor, env.IsLittleEndian
+  );
+  lView.setFloat32(
+    aMeasureData + WASMMEASURE_DESCENDER*SizeFloat32,
+    Desc*scaleFactor, env.IsLittleEndian
+  );
+  Result := ECANVAS_SUCCESS;
+end;
+
+function TWasmFresnelSharedApi.SetTextShadowParams(const aID: TOffscreenCanvasID;
+ aOffsetX, aOffsetY, aRadius: TFresnelFloat; aRed, aGreen, aBlue,
+ aAlpha: TCanvasColorComponent): TCanvasError;
+
+var
+  Canv : TJSOffscreenCanvasRenderingContext2D;
+
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SetTextShadowParams(%d,(%g,%g),%g,"%s")',[aID,aOffsetX,aOffsetY,aRadius,TFresnelHelper.FresnelColorToHTMLColor(aRed,aGreen,aBlue,aAlpha)]);
+  {$ENDIF}
+  Canv:=GetOffscreenContext2D(aID);
+  if Not Assigned(Canv) then
+    Exit(ECANVAS_NOCANVAS);
+  Canv.shadowOffsetX:=aOffsetX;
+  Canv.shadowOffsetY:=aOffsetY;
+  Canv.shadowBlur:=aRadius;
+  Canv.shadowColor:=TFresnelHelper.FresnelColorToHTMLColor(aRed,aGreen,aBlue,aAlpha);
+  Result:=ECANVAS_SUCCESS;
+end;
+
+// CreatePath2D
+//
+function TWasmFresnelSharedApi.CreatePath2D(aFlags : LongInt; aPathCount : LongInt; aPath : PFresnelFloat) : TJSPath2D;
+var
+  LView : TJSDataView;
+  LPtr, LPtrTail : TWasmPointer;
+  aType : TFresnelFloat;
+  X, Y, X1, Y1, X2, Y2 : TFresnelFloat;
+
+  procedure GetTriple;
+  begin
+    aType := LView.getFloat32(LPtr, env.IsLittleEndian);
+    X := LView.getFloat32(LPtr + SizeFloat32, env.IsLittleEndian);
+    Y := LView.getFloat32(LPtr + 2*SizeFloat32, env.IsLittleEndian);
+    Inc(LPtr, 3*SizeFloat32);
+  end;
+
+begin
+  Result := TJSPath2D.New;
+
+  LView := MemoryDataView;
+  LPtr := aPath;
+  LPtrTail := LPtr + aPathCount * (3 * SizeFloat32);
+  aType := 0;
+
+  while LPtr <= LPtrTail do
+  begin
+    GetTriple;
+    if aType = DRAWPATH_TYPEMOVETO then
+      Result.MoveTo(X,Y)
+    else if aType = DRAWPATH_TYPELINETO then
+      Result.LineTo(X,Y)
+    else if aType = DRAWPATH_TYPECURVETO then
+    begin
+      X1:=X;
+      Y1:=Y;
+      GetTriple;
+      if aType <> DRAWPATH_TYPECURVETO then
+      begin
+        Console.Error('Invalid path data 2, expected CURVETO (',DRAWPATH_TYPECURVETO,'), got: ',aType);
+        exit;
+      end;
+      X2:=X;
+      Y2:=Y;
+      GetTriple;
+      if aType <> DRAWPATH_TYPECURVETO then
+      begin
+        Console.Error('Invalid path data 3, expected CURVETO (',DRAWPATH_TYPECURVETO,'), got: ',aType);
+        exit;
+      end;
+      Result.bezierCurveTo(X1, Y1, X2, Y2, X, Y);
+    end
+    else if aType = DRAWPATH_TYPECLOSE then
+    begin
+      Result.ClosePath;
+    end;
+  end;
+  if (aType <> DRAWPATH_TYPECLOSE) and ((aFlags and DRAWPATH_CLOSEPATH) <> 0) then
+    Result.closePath;
+end;
+
+// DrawPath
+//
+function TWasmFresnelSharedApi.DrawPath(const aID: TOffscreenCanvasID;
+ aFlags: LongInt; aPathCount: LongInt; aPath: PFresnelFloat): TCanvasError;
+var
+  LCanvas : TJSOffscreenCanvasRenderingContext2D;
+  LPath2D : TJSPath2D;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.DrawPath(%d,%d,%d,[%x])',[aID,aFlags,aPathCount,aPath]);
+  {$ENDIF}
+  LCanvas := GetOffscreenContext2D(aID);
+  if not Assigned(LCanvas) then
+    Exit(ECANVAS_NOCANVAS);
+  if aPathCount = 0 then
+    Exit(ECANVAS_INVALIDPATH);
+
+  LPath2D := CreatePath2D(aFlags, aPathCount, aPath);
+
+  if (aFlags and DRAWPATH_FILLPATH) <> 0 then
+    LCanvas.Fill(LPath2D);
+  if (aFlags and DRAWPATH_STROKEPATH) <> 0 then
+    LCanvas.Stroke(LPath2D);
+  Result := ECANVAS_SUCCESS;
+end;
+
+// PointInPath
+//
+function TWasmFresnelSharedApi.PointInPath(const aID: TOffscreenCanvasID; aX,
+ aY: TFresnelFloat; aPathCount: Integer; aPath: PFresnelFloat;
+ aRes: TWasmPointer): TCanvasError;
+var
+  LCanvas : TJSOffscreenCanvasRenderingContext2D;
+  LPath2D : TJSPath2D;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.PointInPath(%d,%d,%d,[%x])', [ aID, aFlags, aPathCount, aPath ]);
+  {$ENDIF}
+  LCanvas := GetOffscreenContext2D(aID);
+  if not Assigned(LCanvas) then
+    Exit(ECANVAS_NOCANVAS);
+  if aPathCount = 0 then
+    Exit(ECANVAS_INVALIDPATH);
+
+  LPath2D := CreatePath2D(DRAWPATH_TYPECLOSE, aPathCount, aPath);
+
+  MemoryDataView.setInt8(aRes, Ord(LCanvas.isPointInPath(LPath2D, aX, aY)));
+  Result := ECANVAS_SUCCESS;
+end;
+
+// SetTransform
+//
+function TWasmFresnelSharedApi.SetTransform(
+  const aID: TOffscreenCanvasID;
+  m11, m12, m21, m22, m31, m32: TFresnelFloat
+  ): TCanvasError;
+var
+  lOffscreenRef : TOffscreenCanvasReference;
+  s : Double;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SetTransform(%d,%d,[%g,%g,%g,%g,%g,%g])',[aID,Flags,m11,m12,m21,m22,m31,m32]);
+  {$ENDIF}
+  lOffscreenRef := GetOffscreenCanvasRef(aID);
+  if lOffscreenRef = nil then
+    Exit(ECANVAS_NOCANVAS);
+
+  s := lOffscreenRef.Scale;
+  lOffscreenRef.CanvasContext.setTransform(
+    s * m11, s * m12,
+    s * m21, s * m22,
+    s * m31, s * m32
+  );
+
+  Result := ECANVAS_SUCCESS;
+end;
+
+// SaveState
+//
+function TWasmFresnelSharedApi.SaveState(const aID: TOffscreenCanvasID): TCanvasError;
+var
+  Ref : TOffscreenCanvasReference;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SaveState(%d)',[aID]);
+  {$ENDIF}
+  Ref := GetOffscreenCanvasRef(aID);
+  if not Assigned(Ref) then
+    Exit(ECANVAS_NOCANVAS);
+  Ref.CanvasContext.Save;
+  Result := ECANVAS_SUCCESS;
+end;
+
+// RestoreState
+//
+function TWasmFresnelSharedApi.RestoreState(const aID: TOffscreenCanvasID): TCanvasError;
+var
+  Ref : TOffscreenCanvasReference;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.RestoreState(%d)',[aID]);
+  {$ENDIF}
+  Ref := GetOffscreenCanvasRef(aID);
+  if not Assigned(Ref) then
+    Exit(ECANVAS_NOCANVAS);
+  Ref.CanvasContext.Restore;
+  Result := ECANVAS_SUCCESS;
+end;
+
+// RestoreAndSaveState
+//
+function TWasmFresnelSharedApi.RestoreAndSaveState(const aID : TOffscreenCanvasID) : TCanvasError;
+var
+  Ref : TOffscreenCanvasReference;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.RestoreState(%d)',[aID]);
+  {$ENDIF}
+  Ref := GetOffscreenCanvasRef(aID);
+  if not Assigned(Ref) then
+    Exit(ECANVAS_NOCANVAS);
+  Ref.CanvasContext.Restore;
+  Ref.CanvasContext.Save;
+  Result := ECANVAS_SUCCESS;
+end;
+
+// ClipAddRect
+//
+function TWasmFresnelSharedApi.ClipAddRect(const aID : TOffscreenCanvasID; aX,aY,aWidth,aHeight: TFresnelFloat): TCanvasError;
+var
+  lCanvasRef : TOffscreenCanvasReference;
+  lPath2D : TJSPath2D;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.ClipAddRect(%d,(%g,%g)-(%gx%g))',[aID,aX,aY,aWidth,aHeight]);
+  {$ENDIF}
+  lCanvasRef := GetOffscreenCanvasRef(aID);
+  if lCanvasRef = nil then
+    Exit(ECANVAS_NOCANVAS);
+
+  if daClipRect in DebugAPIs then
+    DrawClipRect(lCanvasRef, aX, aY, aWidth, aHeight);
+
+  lPath2D := TJSPath2D.New;
+  lPath2D.rect(aX, aY, aWidth, aHeight);
+  lCanvasRef.CanvasContext.clip(lPath2D);
+  Result := ECANVAS_SUCCESS;
+end;
+
+// ClipAddRectAndExcludes
+//
+function TWasmFresnelSharedApi.ClipAddPolygon(
+ const aID: TOffscreenCanvasID; APolygonData: TWasmPointer;
+ APolygonCount: Integer; AClipQuadData: TWasmPointer; ANbClipQuads: Integer
+ ): TCanvasError;
+var
+  lCanvasRef : TOffscreenCanvasReference;
+  lContext : TJSOffscreenCanvasRenderingContext2D;
+  lPath2D : TJSPath2D;
+  i : Integer;
+  p : LongInt;
+  lDataView : TJSDataView;
+  lOldStrokeStyle : JSValue;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.ClipAddRect(%d,(%g,%g)-(%gx%g))',[aID,aX,aY,aWidth,aHeight]);
+  {$ENDIF}
+  lCanvasRef := GetOffscreenCanvasRef(aID);
+  if lCanvasRef = nil then
+    Exit(ECANVAS_NOCANVAS);
+
+  lContext := lCanvasRef.CanvasContext;
+
+  lPath2D := TJSPath2D.New;
+
+  lDataView := MemoryDataView;
+
+  if APolygonCount > 0 then
+  begin
+    p := APolygonData;
+    lPath2D.moveTo(lDataView.getFloat32(p, Env.IsLittleEndian), lDataView.getFloat32(p+SizeFloat32, Env.IsLittleEndian));
+    Inc(p, 2*SizeFloat32);
+    for i := 1 to APolygonCount-1 do
+    begin
+      lPath2D.lineTo(lDataView.getFloat32(p, Env.IsLittleEndian), lDataView.getFloat32(p+SizeFloat32, Env.IsLittleEndian));
+      Inc(p, 2*SizeFloat32);
+    end;
+    lPath2D.closePath();
+  end;
+
+  p := aClipQuadData;
+  for i := 0 to aNbClipQuads-1 do
+  begin
+    lPath2D.moveTo(lDataView.getFloat32(p, Env.IsLittleEndian), lDataView.getFloat32(p+SizeFloat32, Env.IsLittleEndian));
+    Inc(p, 2*SizeFloat32);
+    lPath2D.lineTo(lDataView.getFloat32(p, Env.IsLittleEndian), lDataView.getFloat32(p+SizeFloat32, Env.IsLittleEndian));
+    Inc(p, 2*SizeFloat32);
+    lPath2D.lineTo(lDataView.getFloat32(p, Env.IsLittleEndian), lDataView.getFloat32(p+SizeFloat32, Env.IsLittleEndian));
+    Inc(p, 2*SizeFloat32);
+    lPath2D.lineTo(lDataView.getFloat32(p, Env.IsLittleEndian), lDataView.getFloat32(p+SizeFloat32, Env.IsLittleEndian));
+    Inc(p, 2*SizeFloat32);
+    lPath2D.closePath();
+  end;
+
+  if daClipRect in DebugAPIs then
+  begin
+    lOldStrokeStyle := lContext.strokeStyle;
+    lContext.strokeStyle := 'red';
+    lContext.stroke(lPath2D);
+    lContext.strokeStyle := lOldStrokeStyle;
+  end;
+
+  lContext.clip(lPath2D);
+
+  Result := ECANVAS_SUCCESS;
+end;
+
+function TWasmFresnelSharedApi.AllocateTimer(ainterval: LongInt; userdata: TWasmPointer): TTimerID;
+var
+  aTimerID : TTimerID;
+  CallBack : JSValue;
+
+  procedure HandleTimer;
+  var
+    Continue : Boolean;
+  begin
+    // The instance/timer could have disappeared
+    Callback:=InstanceExports['__fresnel_timer_tick'];
+    //Writeln(Format('FresnelAPi.TimerTick(%d)',[aTimerID]));
+    Continue:=Assigned(Callback);
+    if Continue then
+      Continue:=TTimerTickCallback(CallBack)(aTimerID,userData)
+    else
+      Console.Error('No more tick callback !');
+    if not Continue then
+    begin
+      //Writeln(Format('FresnelAPi.TimerTick(%d), return value false, deactivate',[aTimerID]));
+      DeAllocateTimer(aTimerID);
+    end;
+  end;
+
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('FresnelApi.AllocateTimer(%d,[%x])',[aInterval,UserData]);
+  {$ENDIF}
+  Callback:=InstanceExports['__fresnel_timer_tick'];
+  if not Assigned(Callback) then
+    Exit(0);
+  aTimerID := Self_.setInterval(@HandleTimer,aInterval);
+  Result := aTimerID;
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('FresnelApi.AllocateTimer(%d,[%x] => %d)',[aInterval,UserData]);
+  {$ENDIF}
+end;
+
+procedure TWasmFresnelSharedApi.DeallocateTimer(timerid: TTimerID);
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('FresnelApi.DeAllocateTimer(%d)',[TimerID]);
+  {$ENDIF}
+  Self_.clearInterval(TimerID);
+end;
+
+// GetEvent
+//
+function TWasmFresnelSharedApi.GetEvent(aID: TWasmPointer; aMsg: TWasmPointer; Data: TWasmPointer): TCanvasError;
+var
+  view : TJSDataView;
+  evt : TWindowEvent;
+begin
+  evt := DequeueEvent;
+  if evt = nil then
+    Exit(EWASMEVENT_NOEVENT);
+
+  view := MemoryDataView;
+  view.setInt32(aID,  evt.WindowID, env.IsLittleEndian);
+  view.setInt32(aMsg, evt.Msg,      env.IsLittleEndian);
+
+  view.setInt32(Data,               evt.param0, env.IsLittleEndian);
+  view.setInt32(Data + SizeInt32,   evt.param1, env.IsLittleEndian);
+  view.setInt32(Data + 2*SizeInt32, evt.param2, env.IsLittleEndian);
+  view.setInt32(Data + 3*SizeInt32, evt.param3, env.IsLittleEndian);
+
+  RecycleEvent(evt);
+
+  Result := EWASMEVENT_SUCCESS;
+end;
+
+// GetEventCount
+//
+function TWasmFresnelSharedApi.GetEventCount(aCount: TWasmPointer): TCanvasError;
+var
+  view : TJSDataView;
+begin
+  view := MemoryDataView;
+  view.setint32(aCount, EventCount, env.IsLittleEndian);
+  Result := EWASMEVENT_SUCCESS;
+end;
+
+// ConsoleLog
+//
+procedure TWasmFresnelSharedApi.ConsoleLog(aText: TWasmPointer; aTextLen: integer);
+begin
+  WebAssemblyMemory := Env.Memory.buffer;
+  Console.log(GetUTF16FromMem(aText, aTextLen));
+  WebAssemblyMemory := nil;
+end;
+
+// GetEnumeratedUserMedia
+//
+function TWasmFresnelSharedApi.GetEnumeratedUserMedia(aUTF16SizePtr, aDataUTF16: TWasmPointer): TCanvasError;
+var
+  view : TJSDataView;
+  allocatedSize : Integer;
+  requiredSize : Integer;
+begin
+  requiredSize := Length(FEnumeratedUserMedia);
+
+  view := MemoryDataView;
+  allocatedSize := view.getInt32(aUTF16SizePtr, env.IsLittleEndian);
+
+  view.setInt32(aUTF16SizePtr, requiredSize, env.IsLittleEndian);
+
+  if allocatedSize < requiredSize then
+    Exit(EWASMEVENT_BUFFER_SIZE);
+
+  SetUTF16ToMem(aDataUTF16, FEnumeratedUserMedia);
+  Result := EWASMEVENT_SUCCESS;
+end;
+
+end.
+

+ 409 - 0
src/pas2js/fresnel.simple.pas2js.wasmapi.pp

@@ -0,0 +1,409 @@
+{
+    This file is part of the Fresnel Library.
+    Copyright (c) 2025 by the FPC & Lazarus teams.
+
+    Pas2js Fresnel interface - Webassembly rendering API, simple main browser
+
+    See the file COPYING.modifiedLGPL.txt, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+{$mode objfpc}
+{$h+}
+{$modeswitch externalclass}
+{$modeswitch advancedrecords}
+
+{$DEFINE IMAGE_USEOSC}
+
+// Fresnel API to use when everything is running in the main thread
+
+unit fresnel.simple.pas2js.wasmapi;
+
+interface
+
+// Define this to disable API Logging altogether
+{$DEFINE NOLOGAPICALLS}
+
+// Define this to publish OffscreenCanvases to a window.fresnelOffscreen, main-thread only
+{ $DEFINE PUBLISH_FRESNEL_OFFSCREEN}
+
+// Define this to enable rendering statistics
+{ $DEFINE ENABLE_RENDERING_STATISTICS}
+
+uses
+  Classes, SysUtils, Types,
+  Web, JS, wasienv, webassembly,
+  fresnel.keys,
+  fresnel.wasm.shared,
+  fresnel.messages.pas2js.wasmapi,
+  fresnel.shared.pas2js,
+  fresnel.menubuilder.pas2js.wasmapi,
+  fresnel.browser.pas2js.wasmapi;
+
+type
+
+  { TWasmFresnelSimpleApi }
+
+  TWasmFresnelSimpleApi = class(TWasmFresnelBrowserApi)
+  private
+    {$ifdef ENABLE_RENDERING_STATISTICS}
+    FStats_NbRAF : Integer;
+    FStats_NbDOCOW : Integer;
+    {$endif}
+
+  protected
+    procedure DoTimerTick; override;
+
+  public
+    constructor Create(aEnv : TPas2JSWASIEnvironment); override;
+
+    procedure FillImportObject(aObject : TJSObject); override;
+
+    // WindowCanvas
+
+    function GetCanvasRect(const aID : TWindowCanvasID; aRectF: PFresnelFloat): TCanvasError;
+    function SetCanvasRect(const aID : TWindowCanvasID; aLeft, aTop, aRight, aBottom: TFresnelFloat): TCanvasError;
+
+    function GetCanvasSize(const aID : TWindowCanvasID; aPointF: PFresnelFloat): TCanvasError;
+    function SetCanvasSize(const aID : TWindowCanvasID; aWidth, aHeight: TFresnelFloat): TCanvasError;
+
+    procedure DrawOffscreenCanvasOnWindowCanvas(const aWindowID : TWindowCanvasID; const aCanvasID : TOffscreenCanvasID);
+
+    function GetViewPortSizes(Flags : LongInt; aWidth, aHeight : PFresnelFloat) : TCanvasError;
+
+    // Menu
+
+    function HandleMenuClick(aMenuID : TMenuID; aData : TWasmPointer) : Boolean; override;
+
+    // RequestAnimationFrame
+
+    procedure RequestAnimationFrame(userdata: TWasmPointer);
+
+    // UserMedia
+
+    function EnumerateUserMedia(aUserData : TWasmPointer) : TCanvasError;
+
+  end;
+
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+implementation
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+
+// ---------------
+// --------------- TWasmFresnelApi ---------------
+// ---------------
+
+// Create
+//
+constructor TWasmFresnelSimpleApi.Create(aEnv: TPas2JSWASIEnvironment);
+begin
+  inherited Create(aEnv);
+
+  {$IFDEF PUBLISH_FRESNEL_OFFSCREEN}
+  window['fresnelOffscreen'] := FOffscreenCanvases;
+  {$ENDIF}
+
+  {$ifdef ENABLE_RENDERING_STATISTICS}
+  Window.setInterval(
+    procedure
+    begin
+      Document.title := Format('Raf: %1.f   DoCoW: %.1f', [ FStats_NbRAF /2, FStats_NbDOCOW / 2 ]);
+      FStats_NbRAF := 0;
+      FStats_NbDOCOW := 0;
+    end, 2000
+  );
+  {$endif}
+end;
+
+// FillImportObject
+//
+procedure TWasmFresnelSimpleApi.FillImportObject(aObject: TJSObject);
+begin
+  inherited FillImportObject(aObject);
+
+  FImportObject := aObject;
+
+  // Window
+
+  aObject['canvas_allocate_window'] := @AllocateWindowCanvas;
+  aObject['canvas_deallocate_window'] := @DeAllocateWindowCanvas;
+
+  aObject['window_show_hide'] := @ShowHideWindow;
+
+  aObject['canvas_draw_offscreen_on_window'] := @DrawOffscreenCanvasOnWindowCanvas;
+
+  aObject['canvas_getrect'] := @GetCanvasRect;
+  aObject['canvas_setrect'] := @SetCanvasRect;
+  aObject['canvas_getsize'] := @GetCanvasSize;
+  aObject['canvas_setsize'] := @SetCanvasSize;
+
+  aObject['canvas_set_title'] := @SetWindowTitle;
+
+  aObject['canvas_get_viewport_sizes'] := @GetViewPortSizes;
+
+  // Cursor
+
+  aObject['cursor_set'] := @SetCursor;
+  
+  // RequestAnimationFrame
+
+  aObject['request_animation_frame'] := @RequestAnimationFrame;
+
+  // Clipboard
+
+  aObject['clipboard_read_text'] := @ClipboardReadText;
+  aObject['clipboard_write_text'] := @ClipboardWriteText;
+
+  // Event
+
+  aObject['event_set_special_keymap'] := @SetSpecialKeyMap;
+  aObject['wake_main_thread'] := @WakeMainThread;
+
+  // Menu
+
+  aObject['menu_add_item'] := @AddMenuItem;
+  aObject['menu_remove_item'] := @DeleteMenuItem;
+  aObject['menu_update_item'] := @UpdateMenuItem;
+
+  // UserMedia
+
+  aObject['usermedia_enumerate'] := @EnumerateUserMedia;
+  aObject['usermedia_getenumerated'] := @GetEnumeratedUserMedia;
+
+  aObject['usermedia_startcapture'] := @UserMediaStartCapture;
+  aObject['usermedia_stopcapture'] := @UserMediaStopCapture;
+  aObject['usermedia_iscapturing'] := @UserMediaIsCapturing;
+
+end;
+
+// DoTimerTick
+//
+var
+  vLastTimeTimerTick : NativeInt;
+procedure TWasmFresnelSimpleApi.DoTimerTick;
+var
+  Callback : JSValue;
+  T : NativeInt;
+begin
+  T := vLastTimeTimerTick;
+  vLastTimeTimerTick := TJSDate.now;
+  if not assigned(InstanceExports) then
+    Console.log('DoTimerTick: no instance exports !')
+  else
+  begin
+    Callback := InstanceExports['__fresnel_tick'];
+    if Assigned(Callback) then
+      TTimerCallback(CallBack)(vLastTimeTimerTick, T)
+    else
+      Console.warn('DoTimerTick: no tick callback !');
+  end;
+end;
+
+// GetCanvasRect
+//
+function TWasmFresnelSimpleApi.GetCanvasRect(const aID: TWindowCanvasID; aRectF: PFresnelFloat): TCanvasError;
+var
+  lWinRef: TWindowReference;
+  v : TJSDataView;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.GetCanvasRect(%d,[%x])',[aID,aRectF]);
+  {$ENDIF}
+  lWinRef := GetWindowRef(aID);
+  if not Assigned(lWinRef) then
+    Exit(ECANVAS_NOCANVAS);
+  v := MemoryDataView;
+  v.setFloat32(aRectF                , lWinRef.Left,                 env.IsLittleEndian);
+  v.setFloat32(aRectF +   SizeFloat32, lWinRef.Top,                  env.IsLittleEndian);
+  v.setFloat32(aRectF + 2*SizeFloat32, lWinRef.Left + lWinRef.Width, env.IsLittleEndian);
+  v.setFloat32(aRectF + 3*SizeFloat32, lWinRef.Top + lWinRef.Height, env.IsLittleEndian);
+  Result := ECANVAS_SUCCESS;
+end;
+
+// SetCanvasRect
+//
+function TWasmFresnelSimpleApi.SetCanvasRect(const aID: TWindowCanvasID; aLeft, aTop, aRight, aBottom: TFresnelFloat): TCanvasError;
+var
+  Ref: TWindowReference;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SetCanvasRect(%d,[%g,%g,%g,%g])',[aID,aLeft, aTop, aRight, aBottom]);
+  {$ENDIF}
+  Ref := GetWindowRef(aID);
+  if not Assigned(Ref) then
+    Exit(ECANVAS_NOCANVAS);
+  Ref.SetPos(aLeft, aTop);
+  Ref.SetSize(Round(aRight-aLeft), Round(aBottom-aTop));
+end;
+
+// GetCanvasSize
+//
+function TWasmFresnelSimpleApi.GetCanvasSize(const aID: TWindowCanvasID; aPointF: PFresnelFloat): TCanvasError;
+var
+  Ref: TWindowReference;
+  v : TJSDataView;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.GetCanvasSizes(%d,[%x],[%x])',[aID,aWidth,aHeight]);
+  {$ENDIF}
+  Ref := GetWindowRef(aID);
+  if not Assigned(Ref) then
+    Exit(ECANVAS_NOCANVAS);
+  v := MemoryDataView;
+  v.setFloat32(aPointF,               Ref.Width,  env.IsLittleEndian);
+  v.setFloat32(aPointF + SizeFloat32, Ref.Height, env.IsLittleEndian);
+  Result := ECANVAS_SUCCESS;
+end;
+
+// SetCanvasSize
+//
+function TWasmFresnelSimpleApi.SetCanvasSize(const aID: TWindowCanvasID; aWidth, aHeight: TFresnelFloat): TCanvasError;
+var
+  Ref: TWindowReference;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SetCanvasSizes(%d,%d,%d)', [aID,aWidth,aHeight]);
+  {$ENDIF}
+  Ref := GetWindowRef(aID);
+  if not Assigned(Ref) then
+    Exit(ECANVAS_NOCANVAS);
+  Ref.SetSize(aWidth, aHeight);
+
+  Result := ECANVAS_SUCCESS;
+end;
+
+// DrawOffscreenCanvasOnWindowCanvas
+//
+procedure TWasmFresnelSimpleApi.DrawOffscreenCanvasOnWindowCanvas(const aWindowID: TWindowCanvasID; const aCanvasID: TOffscreenCanvasID);
+var
+  srcCanvas : TJSHTMLOffscreenCanvasElement;
+  destWindow : TWindowReference;
+  message : TJSObject;
+begin
+  srcCanvas := GetOffscreenCanvasRef(aCanvasID).Canvas;
+
+  destWindow := GetWindowRef(aWindowID);
+  destWindow.CanvasContext.drawImage(srcCanvas, 0, 0);
+
+  {$ifdef ENABLE_RENDERING_STATISTICS}
+  Inc(FStats_NbDOCOW);
+  {$endif}
+end;
+
+// GetViewPortSizes
+//
+function TWasmFresnelSimpleApi.GetViewPortSizes(Flags: LongInt; aWidth, aHeight: PFresnelFloat): TCanvasError;
+var
+  W,H : NativeInt;
+  v : TJSDataView;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.GetViewPortSizes(%d,[%x],[%x])',[Flags,aWidth,aHeight]);
+  {$ENDIF}
+  if Flags=GETVIEWPORTSIZE_CLIENT then
+  begin
+    W:=document.documentElement.clientWidth;
+    H:=document.documentElement.clientHeight;
+  end
+  else if FLAGS=GETVIEWPORTSIZE_WINDOW then
+  begin
+    W:=Window.innerWidth;
+    H:=document.documentElement.clientHeight;
+  end
+  else
+  begin
+    W:=0;
+    H:=0;
+  end;
+  if (W=0) or (H=0) then
+    Result:=ECANVAS_INVALIDPARAM
+  else
+  begin
+    V:=MemoryDataView;
+    v.setFloat32(aWidth,  W, env.IsLittleEndian);
+    v.setFloat32(aHeight, H, env.IsLittleEndian);
+    Result:=ECANVAS_SUCCESS;
+  end;
+end;
+
+// HandleMenuClick
+//
+function TWasmFresnelSimpleApi.HandleMenuClick(aMenuID: TMenuID; aData: TWasmPointer): Boolean;
+var
+  Callback : JSValue;
+begin
+  Result:=False;
+  if not assigned(InstanceExports) then
+    Console.warn('No instance exports !')
+  else
+  begin
+    Callback:=InstanceExports['__fresnel_menu_click'];
+    if Assigned(Callback) then
+    begin
+      TMenuClickCallback(CallBack)(aMenuID,AData);
+      Result:=True;
+    end
+    else
+      Console.warn('No menu click callback !');
+  end;
+end;
+
+// RequestAnimationFrame
+//
+procedure TWasmFresnelSimpleApi.RequestAnimationFrame(userdata: TWasmPointer);
+
+  procedure HandleAnimationFrame(timestamp{%H-} : Double);
+  var
+    lCallbackValue : JSValue;
+    lCallback : TAnimationFrameCallback absolute lCallbackValue;
+  begin
+    lCallbackValue := InstanceExports['__fresnel_animation_frame'];
+    if lCallbackValue then
+      lCallback();
+  end;
+
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('FresnelApi.RequestAnimationFrame');
+  {$ENDIF}
+
+  window.requestAnimationFrame(@HandleAnimationFrame);
+
+  {$ifdef ENABLE_RENDERING_STATISTICS}
+  Inc(FStats_NbRAF);
+  {$endif}
+end;
+
+// EnumerateUserMedia
+//
+function TWasmFresnelSimpleApi.EnumerateUserMedia(aUserData: TWasmPointer): TCanvasError;
+begin
+  FEnumeratedUserMedia := '';
+  DoEnumerateUserMedia(
+    procedure
+    var
+      callback : JSValue;
+    begin
+      callback := InstanceExports['__fresnel_usermedia_enumerated'];
+      if callback then
+        TUserMediaCallback(callback)(Length(FEnumeratedUserMedia), aUserData);
+    end
+  );
+end;
+
+end.
+

+ 452 - 0
src/pas2js/fresnel.wasm.shared.pp

@@ -0,0 +1,452 @@
+{
+    This file is part of the Fresnel Library.
+    Copyright (c) 2024 by the FPC & Lazarus teams.
+
+    Webassembly rendering - common consts for Fresnel
+
+    See the file COPYING.modifiedLGPL.txt, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+
+unit fresnel.wasm.shared;
+
+{$mode objfpc}{$H+}
+{$modeswitch typehelpers}
+{$modeswitch advancedrecords}
+
+interface
+
+uses
+  {$IFDEF FPC_DOTTEDUNITS}
+  System.Classes, System.SysUtils;
+  {$ELSE}
+  Classes, SysUtils;
+  {$ENDIF}
+
+const
+  WindowMsgSize = 4;
+  CanvasMeasureTextSize = 4;
+  FresnelScaleFactor = 100;
+
+Type
+  {$IFNDEF PAS2JS}
+  TFresnelFloat = single;
+  {$ELSE}
+  TFresnelFloat = double;
+  {$ENDIF}
+  TFresnelFloatArray = Array of TFresnelFloat;
+
+  TCanvasError = longint;
+
+  { TWindowCanvasID }
+
+  // ID for a Canvas that exists in the DOM and corresponds to a Window
+  {$ifdef PAS2JS}
+  // using Int16 here to work around type helper limitation
+  // (type helper is bound to the unaliased type, so we need a different type than for TOffscreenCanvasID)
+  TWindowCanvasID = Int16;
+  {$else}
+  TWindowCanvasID = record
+    Win : LongInt;
+  end;
+  {$endif}
+
+  TWindowCanvasIDHelper = {$ifdef PAS2JS}type{$else}record{$endif} helper for TWindowCanvasID
+    function ToIDString : String; inline;
+    function ToString : String;
+  end;
+
+  { TOffscreenCanvasID }
+
+  // ID for an OffscreenCanvas that can exist in main thread or webworker
+  {$ifdef PAS2JS}
+  TOffscreenCanvasID = Int32;
+  {$else}
+  TOffscreenCanvasID = record
+    Cnv : LongInt;
+  end;
+  {$endif}
+
+  TOffscreenCanvasIDHelper = {$ifdef PAS2JS}type{$else}record{$endif} helper for TOffscreenCanvasID
+    function ToIDString : String; inline;
+    function ToString : String;
+  end;
+
+  { TVideoElementID }
+  // ID for an OffscreenCanvas that can exist in main thread or webworker
+  {$ifdef PAS2JS}
+  TVideoElementID = Int32;
+  {$else}
+  TVideoElementID = record
+    Vid : LongInt;
+  end;
+  {$endif}
+
+  TVideoElementIDHelper = {$ifdef PAS2JS}type{$else}record{$endif} helper for TVideoElementID
+    function ToIDString : String; inline;
+    function ToString : String;
+  end;
+
+const
+   // not the default value for ID, but the ID of the default canvas
+  // which is used as fallback in text measurer & others
+   cOffscreenCanvasDefaultCanvasID = 1;
+
+type
+
+  TCanvasColorComponent = Word; // one of R G B A
+  TCanvasColor = longint;
+  TCanvasLineWidth = TFresnelFloat;
+  TCanvasLineCap = byte;
+  TCanvasLineJoin = byte;
+  TCanvasTextBaseLine = Byte;
+  TCanvasLineMiterLimit = TFresnelFloat;
+  TMenuID = longint;
+
+
+  TWindowMessageID = longint;
+  TWindowMessageParam = longint;
+  TWindowMessageData = array [0..WindowMsgSize-1] of TWindowMessageParam;
+
+  TCanvasMeasureTextParam = TFresnelFloat;
+  TCanvasMeasureTextData = array [0..CanvasMeasureTextSize] of TCanvasMeasureTextParam;
+
+  TLineDashPattern = TFresnelFloat;
+  TLineDashPatternData = array of TLineDashPattern;
+
+  {$IFDEF PAS2JS}
+  UTF8String = String;
+  {$ENDIF}
+
+  TTimerID = longint;
+  TCanvasRoundRectData = Array[0..11] of TFresnelFloat;
+
+  { TGradientColorPoint }
+
+  TGradientColorPoint = record
+    Red,Green,Blue,Alpha : longint;
+    Percentage : longint; // Scaled 100
+    function ToString : string;
+  end;
+  TGradientColorPoints = Array of TGradientColorPoint;
+
+  { TWindowMessageDataHelper }
+
+  TWindowMessageDataHelper = type helper for TWindowMessageData
+    function ToString : UTF8String;
+  end;
+
+  {$IFNDEF PAS2JS}
+  PFresnelFloat = ^TFresnelFloat;
+  PWindowCanvasID = ^TWindowCanvasID;
+  POffscreenCanvasID = ^TOffscreenCanvasID;
+  PCanvasColor = ^TCanvasColor;
+  PWindowMessageID = ^TWindowMessageID;
+  PWindowMessageData = ^TWindowMessageData;
+  PCanvasMeasureTextData = ^TCanvasMeasureTextData;
+  PCanvasRoundRectData = ^TCanvasRoundRectData;
+  PGradientColorPoints = ^TGradientColorPoint;
+  PLineDashPatternData = ^TLineDashPattern;
+  PKeyMap = PLongint;
+  TWasmPointer = Pointer;
+  PMenuID = ^TMenuID;
+  {$ELSE}
+  TWasmPointer = Longint;
+  PFresnelFloat = TWasmPointer;
+  PCanvasID = TWasmPointer;
+  PCanvasColor = TWasmPointer;
+  PCanvasMessageID = TWasmPointer;
+  PCanvasMessageData = TWasmPointer;
+  PCanvasMeasureTextData = TWasmPointer;
+  PCanvasRoundRectData = TWasmPointer;
+  PGradientColorPoints = TWasmPointer;
+  PLineDashPatternData = TWasmPointer;
+  PKeyMap = TWasmPointer;
+  PMenuID = TWasmPointer;
+  {$ENDIF}
+
+Const
+  ECANVAS_SUCCESS       =  0;
+  ECANVAS_NOWINDOW      = 10;
+  ECANVAS_NOCANVAS      = 11;
+  ECANVAS_NOIMAGEBITMAP = 12;
+  ECANVAS_NOVIDEO       = 13;
+  ECANVAS_INVALIDPATH   = 20;
+  ECANVAS_INVALIDPARAM  = 21;
+  ECANVAS_NOMENUSUPPORT = 22;
+  ECANVAS_OFFSCREEN     = 30;
+  ECANVAS_EXCEPTION     = 40;
+  ECANVAS_UNSPECIFIED   = -1;
+
+  CANVAS_LINECAP_BUTT   = 0;
+  CANVAS_LINECAP_ROUND  = 1;
+  CANVAS_LINECAP_SQUARE = 2;
+
+  CANVAS_LINEJOIN_ROUND = 0;
+  CANVAS_LINEJOIN_BEVEL = 1;
+  CANVAS_LINEJOIN_MITER = 2;
+
+  EWASMEVENT_SUCCESS     = 0;
+  EWASMEVENT_NOEVENT     = 1;
+  EWASMEVENT_NOCANVAS    = 2;
+  EWASMEVENT_BUFFER_SIZE = 3;
+  EWASMEVENT_TRY_AGAIN   = 4;
+  EWASMEVENT_ERROR       = 5;
+
+  // Key state, Based on TShiftStateEnum
+  WASM_KEYSTATE_SHIFT   = 1 shl Ord(ssShift);
+  WASM_KEYSTATE_CTRL    = 1 shl Ord(ssAlt);
+  WASM_KEYSTATE_ALT     = 1 shl Ord(ssCtrl);
+  WASM_KEYSTATE_LEFT    = 1 shl Ord(ssLeft);
+  WASM_KEYSTATE_RIGHT   = 1 shl Ord(ssRight);
+  WASM_KEYSTATE_MIDDLE  = 1 shl Ord(ssMiddle);
+  WASM_KEYSTATE_DOUBLE  = 1 shl Ord(ssDouble);
+  WASM_KEYSTATE_META    = 1 shl Ord(ssMeta);
+  WASM_KEYSTATE_SUPER   = 1 shl Ord(ssSuper);
+  WASM_KEYSTATE_HYPER   = 1 shl Ord(ssHyper);
+  WASM_KEYSTATE_ALTGR   = 1 shl Ord(ssAltGr);
+
+  // Location of mouse state data in parameters array
+  WASMSG_MOUSESTATE_X        = 0;
+  WASMSG_MOUSESTATE_Y        = 1;
+  WASMSG_MOUSESTATE_STATE    = 2;
+  WASMSG_MOUSESTATE_BUTTON   = 3;
+  WASMSG_MOUSESTATE_DISTANCE = 3;
+
+  // location of key state data in parameters array
+  WASMSG_KEYSTATE_KEYCODE    = 0;
+  WASMSG_KEYSTATE_KIND       = 1;
+  WASMSG_KEYSTATE_SHIFTSTATE = 2;
+
+  WASMSG_KEYKIND_CHAR    = 0;
+  WASMSG_KEYKIND_SPECIAL = 1;
+
+  // Location of data in measuretext array
+  WASMMEASURE_WIDTH     = 0;
+  WASMMEASURE_HEIGHT    = 1;
+  WASMMEASURE_ASCENDER  = 2;
+  WASMMEASURE_DESCENDER = 3;
+
+Const
+  WASMSG_NONE        = 0;
+  WASMSG_MOVE        = 1; // Params[0]= X, [1]=Y, [2]=State
+  WASMSG_MOUSEDOWN   = 2; // Params[0]= X, [1]=Y, [2]=State
+  WASMSG_MOUSEUP     = 3; // Params[0]= X, [1]=Y, [2]=State
+  WASMSG_MOUSESCROLL = 4; // Params[0]= X, [1]=Y, [2]=State
+  WASMSG_CLICK       = 5; // Params[0]= X, [1]=Y, [2]=State
+  WASMSG_WHEELY      = 6; // Params[0]= X, [1]=Y, [2]=State [3]=Distance
+  WASMSG_DBLCLICK    = 7; // Params[0]= X, [1]=Y, [2]=State
+  WASMSG_ENTER       = 8;
+  WASMSG_LEAVE       = 9;
+  WASMSG_KEYDOWN     = 10;
+  WASMSG_KEYUP       = 11;
+  WASMSG_ACTIVATE    = 12;
+  WASMSG_DEACTIVATE  = 13;
+  WASMSG_RESIZE      = 14; // Params[0]= X, [1]=Y, [2]=PixelRatio*1000 (XY in virtual pixels)
+
+  // brodcast messages are sent from this ID up,
+  // they are dispatched to all windows from topmost down
+  // if one window handles it, broadcasting stops there
+  WASMSG_BROADCAST   = 1000;
+
+  // Roundrect flags
+  ROUNDRECT_FLAG_FILL         = 1;
+
+  // Indexes for roundrect data array.
+
+  ROUNDRECT_BOXTOPLEFTX       = 0;
+  ROUNDRECT_BOXTOPLEFTY       = 1;
+  ROUNDRECT_BOXBOTTOMRIGHTX   = 2;
+  ROUNDRECT_BOXBOTTOMRIGHTY   = 3;
+  ROUNDRECT_RADIITOPLEFTX     = 4;
+  ROUNDRECT_RADIITOPLEFTY     = 5;
+  ROUNDRECT_RADIITOPRIGHTX    = 6;
+  ROUNDRECT_RADIITOPRIGHTY    = 7;
+  ROUNDRECT_RADIIBOTTOMLEFTX  = 8;
+  ROUNDRECT_RADIIBOTTOMLEFTY  = 9;
+  ROUNDRECT_RADIIBOTTOMRIGHTX = 10;
+  ROUNDRECT_RADIIBOTTOMRIGHTY = 11;
+
+  // Flags for SetImageFillStyle
+  IMAGEFILLSTYLE_NOREPEAT  = 0;
+  IMAGEFILLSTYLE_REPEAT    = 1;
+  IMAGEFILLSTYLE_REPEATX   = 2;
+  IMAGEFILLSTYLE_REPEATY   = 3;
+
+  DRAWIMAGE_DESTX       = 0;
+  DRAWIMAGE_DESTY       = 1;
+  DRAWIMAGE_DESTWIDTH   = 2;
+  DRAWIMAGE_DESTHEIGHT  = 3;
+  DRAWIMAGE_SRCX        = 4;
+  DRAWIMAGE_SRCY        = 5;
+  DRAWIMAGE_SRCWIDTH    = 6;
+  DRAWIMAGE_SRCHEIGHT   = 7;
+  DRAWIMAGE_IMAGEWIDTH  = 8;
+  DRAWIMAGE_IMAGEHEIGHT = 9;
+
+  // Flags for Arc
+  ARC_FILL   = 1;
+  ARC_ROTATE = 2;
+
+  // Flags for DrawPath
+  DRAWPATH_CLOSEPATH  = 1;
+  DRAWPATH_FILLPATH   = 2;
+  DRAWPATH_STROKEPATH = 4;
+
+  // Point types in DrawPath
+  DRAWPATH_TYPEMOVETO  = 0;
+  DRAWPATH_TYPELINETO  = 1;
+  DRAWPATH_TYPECURVETO = 2;
+  DRAWPATH_TYPECLOSE   = 3;
+
+  // Which window size ?
+  GETVIEWPORTSIZE_CLIENT = 0;
+  GETVIEWPORTSIZE_WINDOW = 1;
+
+  // Baseline for text
+  TEXTBASELINE_TOP         = 0;
+  TEXTBASELINE_HANGING     = 1;
+  TEXTBASELINE_MIDDLE      = 2;
+  TEXTBASELINE_ALPHABETIC  = 3;
+  TEXTBASELINE_IDEOGRAPHIC = 4;
+  TEXTBASELINE_BOTTOM      = 5;
+
+  // Menu flags
+  MENU_FLAGS_INVISIBLE = 1;
+  MENU_FLAGS_CHECKED = 2;
+  MENU_FLAGS_RADIO   = 4;
+
+  // User Media flags
+  cUserMediaCaptureOption_RequestAnimationFrame = 1;
+  cUserMediaCaptureOption_SelfieSegmentationBlur = 2;
+
+Function LineCapToString(aCap: TCanvasLineCap) : String;
+Function LineJoinToString(aJoin: TCanvasLineJoin) : String;
+Function TextBaseLineToString(aBaseLine : TCanvasTextBaseLine) : String;
+
+{
+Function FresnelUnScale(aLen : Longint) : TFresnelFloat;
+Function FresnelScale(aLen : TFresnelFloat) : Longint;
+}
+
+implementation
+
+{
+Function FresnelUnScale(aLen : Longint) : TFresnelFloat;
+
+begin
+  Result:=aLen/FresnelScaleFactor;
+end;
+
+Function FresnelScale(aLen : TFresnelFloat) : Longint;
+
+begin
+  Result:=Round(aLen*FresnelScaleFactor);
+end;
+}
+
+function LineCapToString(aCap: TCanvasLineCap): String;
+
+begin
+  Case aCap of
+    CANVAS_LINECAP_BUTT : Result:='butt';
+    CANVAS_LINECAP_ROUND : Result:='round';
+    CANVAS_LINECAP_SQUARE : Result:='square';
+  else
+    Result:='butt';
+  end;
+end;
+
+Function LineJoinToString(aJoin: TCanvasLineJoin) : String;
+
+begin
+  Case aJoin of
+    CANVAS_LINEJOIN_ROUND : Result:='round';
+    CANVAS_LINEJOIN_MITER : Result:='miter';
+    CANVAS_LINEJOIN_BEVEL : Result:='bevel';
+  else
+    Result:='round';
+  end;
+end;
+
+Function TextBaseLineToString(aBaseLine : TCanvasTextBaseLine) : String;
+
+begin
+  Case aBaseLine of
+    TEXTBASELINE_TOP : Result:='top';
+    TEXTBASELINE_HANGING : Result:='hanging';
+    TEXTBASELINE_MIDDLE : Result:='middle';
+    TEXTBASELINE_ALPHABETIC : Result:='alphabetic';
+    TEXTBASELINE_IDEOGRAPHIC : Result:='ideographic';
+    TEXTBASELINE_BOTTOM : Result:='bottom';
+  else
+    Result:='alphabetic';
+  end;
+end;
+
+{ TWindowCanvasIDHelper }
+
+function TWindowCanvasIDHelper.ToIDString: String;
+begin
+  Result := IntToStr({$ifdef PAS2JS}Self{$else}Win{$endif});
+end;
+
+function TWindowCanvasIDHelper.ToString: String;
+begin
+  Result := 'Window' + ToIDString;
+end;
+
+{ TOffscreenCanvasIDHelper }
+
+function TOffscreenCanvasIDHelper.ToIDString: String;
+begin
+  Result := IntToStr({$ifdef PAS2JS}Self{$else}Cnv{$endif});
+end;
+
+function TOffscreenCanvasIDHelper.ToString: String;
+begin
+  Result := 'Canvas' + ToIDString;
+end;
+
+{ TVideoElementIDHelper }
+
+function TVideoElementIDHelper.ToIDString: String;
+begin
+  Result := IntToStr({$ifdef PAS2JS}Self{$else}Vid{$endif});
+end;
+
+function TVideoElementIDHelper.ToString: String;
+begin
+  Result := 'Video' + ToIDString;
+end;
+
+{ TGradientColorPoint }
+
+function TGradientColorPoint.ToString: string;
+begin
+  Result := String(Format('{%g%% (r:%d, g:%d, b:%d / %d)}',[Percentage/100,Red,Green,Blue,Alpha]));
+end;
+
+{ TWindowMessageDataHelper }
+
+function TWindowMessageDataHelper.ToString: UTF8String;
+var
+  I : Integer;
+  buffer : String;
+begin
+  buffer  := IntToStr(Self[0]);
+  For I:=1 to WindowMsgSize-1 do
+  begin
+    buffer :=buffer +',';
+    buffer :=buffer +IntToStr(Self[I])
+  end;
+  Result := UTF8String('['+buffer +']');
+end;
+
+end.
+

+ 671 - 0
src/pas2js/fresnel.web.pas2js.wasmapi.pp

@@ -0,0 +1,671 @@
+{
+    This file is part of the Fresnel Library.
+    Copyright (c) 2025 by the FPC & Lazarus teams.
+
+    Pas2js Fresnel interface - Webassembly rendering API
+
+    See the file COPYING.modifiedLGPL.txt, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+{$mode objfpc}
+{$h+}
+{$modeswitch externalclass}
+{$modeswitch advancedrecords}
+
+{$DEFINE IMAGE_USEOSC}
+
+unit fresnel.web.pas2js.wasmapi;
+
+interface
+
+// Define this to disable API Logging altogether
+{$DEFINE NOLOGAPICALLS}
+
+// Define this to enable rendering statistics
+{ $DEFINE ENABLE_RENDERING_STATISTICS}
+
+uses
+  Classes, SysUtils, Types,
+  WebOrWorker, Web, JS, wasienv, webassembly, Rtl.WorkerCommands,
+  fresnel.keys,
+  fresnel.wasm.shared,
+  fresnel.messages.pas2js.wasmapi,
+  fresnel.shared.pas2js,
+  fresnel.menubuilder.pas2js.wasmapi,
+  fresnel.browser.pas2js.wasmapi;
+
+type
+
+  { TWasmFresnelWebApi }
+
+  TWasmFresnelWebApi = class(TWasmFresnelBrowserApi)
+  private
+    FWorker : TJSWorker;
+
+    FEnqueueEventMessage : TFresnelMessage_EnqueueEvent;
+
+    {$ifdef ENABLE_RENDERING_STATISTICS}
+    FStats_NbRAF : Integer;
+    FStats_NbDOCOW : Integer;
+    FStats_NbMessages : Integer;
+    {$endif}
+
+    FPendingRAF : Boolean;
+    function EnumerateUsermedia(aUserData: TWasmPointer): TCanvasError;
+
+  protected
+    procedure DoWorkerMessage_Call(aMessage : TFresnelMessage_FunctionCall);
+    procedure DoWorkerMessage_DrawOffscreenCanvasOnWindow(aMessage : TFresnelMessage_DrawOffscreenCanvasOnWindow);
+    procedure DoWorkerMessage_MenuSupport(aData : TJSObject);
+    procedure DoWorkerMessage_RequestAnimationFrame(aMessage : TFresnelMessage_RequestAnimationFrame);
+    function DrawOffscreenCanvasOnWindow(aWindowID : TWindowCanvasID; aCanvasID : TOffscreenCanvasID) : TCanvasError;
+
+    procedure EnqueueEvent(aEvent : TWindowEvent); override;
+
+    procedure DoTimerTick; override;
+
+    procedure WakeMainThread_ShouldntBeCalledHere;
+
+  public
+    constructor Create(aEnv : TPas2JSWASIEnvironment); override;
+
+    property Worker : TJSWorker read FWorker write FWorker;
+
+    // return True if handled
+    function HandleWorkerMessage(aEvent : TJSMessageEvent) : Boolean;
+
+    procedure FillImportObject(aObject : TJSObject); override;
+
+    // WindowCanvas
+
+    function GetCanvasRect(const aID : TWindowCanvasID; aRectF: PFresnelFloat): TCanvasError;
+    function SetCanvasRect(const aID : TWindowCanvasID; aLeft, aTop, aRight, aBottom: TFresnelFloat): TCanvasError;
+
+    function GetCanvasSize(const aID : TWindowCanvasID; aPointF: PFresnelFloat): TCanvasError;
+    function SetCanvasSize(const aID : TWindowCanvasID; aWidth, aHeight: TFresnelFloat): TCanvasError;
+
+    function GetViewPortSizes(Flags : LongInt; aWidth, aHeight : PFresnelFloat) : TCanvasError;
+
+    procedure RequestAnimationFrame(userdata: TWasmPointer);
+
+    // Menu
+
+    function HandleMenuClick(aMenuID : TMenuID; aData : TWasmPointer) : Boolean; override;
+
+    // UserMedia
+
+    function UserMediaStartCapture(aDeviceID_UTF16 : TWasmPointer; aDeviceID_UTF16Size : Integer;
+                                   aVideoID : TWasmPointer;
+                                   aResolutionWidth, aResolutionHeight : Integer;
+                                   aOptions : Integer
+                                   ) : TCanvasError; override;
+
+    // LocalStorage
+
+    function LocalStorageGetItem(aKey : String; aDataBuffer : TJSSharedArrayBuffer): TCanvasError;
+
+  end;
+
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+implementation
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+
+// ---------------
+// --------------- TWasmFresnelApi ---------------
+// ---------------
+
+// Create
+//
+constructor TWasmFresnelWebApi.Create(aEnv: TPas2JSWASIEnvironment);
+begin
+  inherited Create(aEnv);
+
+  FEnqueueEventMessage := TFresnelMessage_EnqueueEvent.new;
+  FEnqueueEventMessage.Typ := cFresnel_EnqueueEvent;
+
+  {$ifdef ENABLE_RENDERING_STATISTICS}
+  Window.setInterval(
+    procedure
+    begin
+      Document.title := Format(
+        'Raf: %1.f   DoCoW: %.1f   Messages: %.1f',
+        [ FStats_NbRAF /2, FStats_NbDOCOW / 2, FStats_NbMessages / 2 ]
+      );
+      FStats_NbRAF := 0;
+      FStats_NbDOCOW := 0;
+      FStats_NbMessages := 0;
+    end, 2000
+  );
+  {$endif}
+end;
+
+// FillImportObject
+//
+procedure TWasmFresnelWebApi.FillImportObject(aObject: TJSObject);
+begin
+  inherited FillImportObject(aObject);
+
+  // Window
+
+  aObject['canvas_allocate_window'] := @AllocateWindowCanvas;
+  aObject['canvas_deallocate_window'] := @DeAllocateWindowCanvas;
+
+  aObject['window_show_hide'] := @ShowHideWindow;
+
+  aObject['canvas_getrect'] := @GetCanvasRect;
+  aObject['canvas_setrect'] := @SetCanvasRect;
+  aObject['canvas_getsize'] := @GetCanvasSize;
+  aObject['canvas_setsize'] := @SetCanvasSize;
+
+  aObject['canvas_set_title'] := @SetWindowTitle;
+
+  aObject['canvas_get_viewport_sizes'] := @GetViewPortSizes;
+
+  // Cursor
+
+  aObject['cursor_set'] := @SetCursor;
+
+  // Clipboard
+
+  aObject['clipboard_read_text'] := @ClipboardReadText;
+  aObject['clipboard_write_text'] := @ClipboardWriteText;
+
+  // Event
+
+  aObject['event_set_special_keymap'] := @SetSpecialKeyMap;
+  aObject['wake_main_thread'] := @WakeMainThread_ShouldntBeCalledHere;
+
+  // Menu
+
+  aObject['menu_add_item'] := @AddMenuItem;
+  aObject['menu_remove_item'] := @DeleteMenuItem;
+  aObject['menu_update_item'] := @UpdateMenuItem;
+
+  // RequestAnimationFrame
+  aObject['request_animation_frame'] := @RequestAnimationFrame;
+  // UserMedia
+
+  aObject['usermedia_enumerate'] := @EnumerateUserMedia;
+  aObject['usermedia_getenumerated'] := @GetEnumeratedUserMedia;
+
+  aObject['usermedia_startcapture'] := @UserMediaStartCapture;
+  aObject['usermedia_stopcapture'] := @UserMediaStopCapture;
+  aObject['usermedia_iscapturing'] := @UserMediaIsCapturing;
+  aObject['canvas_draw_offscreen_on_window'] := @DrawOffscreenCanvasOnWindow;
+  asm
+  aObject['localstorage_setitem'] = (n, v) => { localStorage.setItem(n, v) };
+  aObject['localstorage_getitem'] = (n) => { return localStorage.getItem(n) };
+  aObject['localstorage_removeitem'] = (n) => { localStorage.removeItem(n) };
+  aObject['localstorage_clear'] = () => { localStorage.clear(); };
+  end;
+  aObject['localstorage_getitem'] := @LocalStorageGetItem;
+
+end;
+
+// DoWorkerMessage_Call
+//
+procedure TWasmFresnelWebApi.DoWorkerMessage_Call(aMessage : TFresnelMessage_FunctionCall);
+var
+  result : TCanvasError;
+  atomicArray : TJSInt32Array;
+begin
+  WebAssemblyMemory := aMessage.Memory;
+  atomicArray := aMessage.Atomic;
+
+  try
+    result := ExecuteFunctionCall(aMessage.FuncName, aMessage.Args);
+    if JSValue(atomicArray) then
+    begin
+      TJSAtomics.store(atomicArray, 1, result);
+      TJSAtomics.store(atomicArray, 0, aMessage.ID);
+    end;
+  except
+    on E: Exception do
+    begin
+      if JSValue(atomicArray) then
+      begin
+        TJSAtomics.store(atomicArray, 1, ECANVAS_EXCEPTION);
+        TJSAtomics.store(atomicArray, 0, aMessage.ID);
+      end;
+      Console.warn('DoWorkerMessage_Call Exception "', E.message, '" for ', TJSJSON.stringify(aMessage));
+    end;
+  end;
+  if JSValue(atomicArray) then
+    TJSAtomics.notify(atomicArray, 0, 1);
+
+  WebAssemblyMemory := nil;
+end;
+
+// DoWorkerMessage_DrawOffscreenCanvasOnWindow
+//
+procedure TWasmFresnelWebApi.DoWorkerMessage_DrawOffscreenCanvasOnWindow(
+ aMessage: TFresnelMessage_DrawOffscreenCanvasOnWindow);
+var
+  atomicArray : TJSInt32Array;
+  result : TCanvasError;
+  windowContext : TJSCanvasRenderingContext2D;
+begin
+  {$ifdef ENABLE_RENDERING_STATISTICS}
+  Inc(FStats_NbDOCOW);
+  {$endif}
+
+  atomicArray := aMessage.Atomic;
+
+  windowContext := GetWindowContext2D(aMessage.WindowID);
+  if windowContext <> nil then
+  begin
+    windowContext.drawImage(aMessage.ImageBitmap, 0, 0);
+    result := ECANVAS_SUCCESS;
+  end
+  else result := ECANVAS_NOCANVAS;
+
+  aMessage.ImageBitmap.close();
+
+  TJSAtomics.store(atomicArray, 1, result);
+  TJSAtomics.store(atomicArray, 0, aMessage.ID);
+
+  TJSAtomics.notify(atomicArray, 0, 1);
+end;
+
+// DoWorkerMessage_MenuSupport
+//
+procedure TWasmFresnelWebApi.DoWorkerMessage_MenuSupport(aData: TJSObject);
+begin
+  MenuSupport := Boolean(aData['value']);
+end;
+
+// DoWorkerMessage_RequestAnimationFrame
+//
+procedure TWasmFresnelWebApi.DoWorkerMessage_RequestAnimationFrame(aMessage : TFresnelMessage_RequestAnimationFrame);
+
+  procedure HandleWorkerAnimationFrame(timestamp : Double);
+  begin
+    {$ifdef ENABLE_RENDERING_STATISTICS}
+    Inc(FStats_NbRAF);
+    {$endif}
+    Worker.postMessage(aMessage);
+    FPendingRAF := False;
+  end;
+
+begin
+  if not FPendingRAF then
+  begin
+    FPendingRAF := True;
+    window.requestAnimationFrame(@HandleWorkerAnimationFrame)
+  end;
+end;
+
+function TWasmFresnelWebApi.DrawOffscreenCanvasOnWindow(aWindowID: TWindowCanvasID; aCanvasID: TOffscreenCanvasID): TCanvasError;
+var
+  windowcontext : TJSBaseCanvasRenderingContext2D;
+  canvasref : TOffscreenCanvasReference;
+begin
+  Writeln('Drawing canvas ID ',aCanvasID,' on window ',aWindowID);
+  windowContext := GetWindowContext2D(aWindowID);
+  if windowContext=nil then
+    exit(ECANVAS_NOCANVAS);
+  canvasref := GetOffscreenCanvasRef(aCanvasID);
+  if canvasref=nil then
+    exit(ECANVAS_NOCANVAS);
+  Writeln('Drawing canvas ID ',aCanvasID,' on window ',aWindowID,' have all data');
+  windowContext.drawImage(canvasref.Canvas, 0, 0);
+  result := ECANVAS_SUCCESS;
+end;
+
+// EnqueueEvent
+//
+procedure TWasmFresnelWebApi.EnqueueEvent(aEvent: TWindowEvent);
+begin
+  if Assigned(FWorker) then
+    begin
+    FEnqueueEventMessage.Event := aEvent;
+    TCommandDispatcher.instance.SendCommand(FWorker,FEnqueueEventMessage);
+    RecycleEvent(aEvent);
+    end
+  else
+    inherited EnqueueEvent(aEvent);
+end;
+
+// DoTimerTick
+//
+procedure TWasmFresnelWebApi.DoTimerTick;
+var
+  lMessage : TFresnelMessage;
+begin
+  lMessage := TFresnelMessage.new;
+  lMessage.Typ := cFresnel_Tick;
+
+  Worker.postMessage(lMessage);
+end;
+
+// WakeMainThread_ShouldntBeCalledHere
+//
+procedure TWasmFresnelWebApi.WakeMainThread_ShouldntBeCalledHere;
+begin
+  // TWasmFresnelWebApi is on the browser side, while WASM main thread is in a
+  // worker and handled by TWasmFresnelWorkerApi.
+  // So this method should never be called unless the wheels have come off...
+  // *but* WASM checks for its presence in the exports, so we need the method
+  console.log('WakeMainThread_ShouldntBeCalledHere');
+end;
+
+// HandleWorkerMessage
+//
+function TWasmFresnelWebApi.HandleWorkerMessage(aEvent: TJSMessageEvent): Boolean;
+
+  procedure HandleUserMediaEnumerated;
+  var
+    lMessage : TFresnelMessage_EnumerateUserMedia;
+  begin
+    lMessage := TFresnelMessage_EnumerateUserMedia(aEvent.Data);
+    lMessage.UserMediaData := FEnumeratedUserMedia;
+    TJSWorker(aEvent.target).postMessage(lMessage);
+  end;
+
+var
+  data: TJSObject;
+  dataType: String;
+  args: TJSValueDynArray;
+  resultMessage: TJSObject;
+begin
+  data := TJSObject(aEvent.data);
+  dataType := String(data['type']);
+
+  {$ifdef ENABLE_RENDERING_STATISTICS}
+  Inc(FStats_NbMessages);
+  //var statName := 'message_' + dataType;
+  //if dataType = cFresnel_Message_Call then
+  //  statName += '_' + TFresnelMessage_FunctionCall(data).FuncName;
+  //if window[statName] then
+  //  window[statName] := Integer(window[statName]) + 1
+  //else window[statName] := 1;
+  {$endif}
+
+  Result := True;
+
+  if dataType = cFresnel_Message_Call then
+
+    DoWorkerMessage_Call(TFresnelMessage_FunctionCall(data))
+
+  else if dataType = cFresnel_Message_DOCOW then
+
+    DoWorkerMessage_DrawOffscreenCanvasOnWindow(TFresnelMessage_DrawOffscreenCanvasOnWindow(data))
+
+  else if dataType = cFresnel_RequestAnimationFrame then
+
+    DoWorkerMessage_RequestAnimationFrame(TFresnelMessage_RequestAnimationFrame(data))
+
+  else if dataType = cFresnel_Message_MenuSupport then
+
+    DoWorkerMessage_MenuSupport(data)
+
+  else if dataType = cFresnel_EnumerateUserMedia then
+  begin
+
+    FEnumeratedUserMedia := '';
+    DoEnumerateUserMedia(@HandleUserMediaEnumerated);
+
+  end
+  else Result := False;
+end;
+
+// GetCanvasRect
+//
+function TWasmFresnelWebApi.GetCanvasRect(const aID: TWindowCanvasID; aRectF: PFresnelFloat): TCanvasError;
+var
+  lWinRef: TWindowReference;
+  v : TJSDataView;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.GetCanvasRect(%d,[%x])',[aID,aRectF]);
+  {$ENDIF}
+  lWinRef := GetWindowRef(aID);
+  if not Assigned(lWinRef) then
+    Exit(ECANVAS_NOCANVAS);
+  v := MemoryDataView;
+  v.setFloat32(aRectF                , lWinRef.Left,                 env.IsLittleEndian);
+  v.setFloat32(aRectF +   SizeFloat32, lWinRef.Top,                  env.IsLittleEndian);
+  v.setFloat32(aRectF + 2*SizeFloat32, lWinRef.Left + lWinRef.Width, env.IsLittleEndian);
+  v.setFloat32(aRectF + 3*SizeFloat32, lWinRef.Top + lWinRef.Height, env.IsLittleEndian);
+  Result := ECANVAS_SUCCESS;
+end;
+
+// SetCanvasRect
+//
+function TWasmFresnelWebApi.SetCanvasRect(const aID: TWindowCanvasID; aLeft, aTop, aRight, aBottom: TFresnelFloat): TCanvasError;
+var
+  Ref: TWindowReference;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SetCanvasRect(%d,[%g,%g,%g,%g])',[aID,aLeft, aTop, aRight, aBottom]);
+  {$ENDIF}
+  Ref := GetWindowRef(aID);
+  if not Assigned(Ref) then
+    Exit(ECANVAS_NOCANVAS);
+  Ref.SetPos(aLeft, aTop);
+  Ref.SetSize(Round(aRight-aLeft), Round(aBottom-aTop));
+end;
+
+// GetCanvasSize
+//
+function TWasmFresnelWebApi.GetCanvasSize(const aID: TWindowCanvasID; aPointF: PFresnelFloat): TCanvasError;
+var
+  Ref: TWindowReference;
+  v : TJSDataView;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.GetCanvasSizes(%d,[%x],[%x])',[aID,aWidth,aHeight]);
+  {$ENDIF}
+  Ref := GetWindowRef(aID);
+  if not Assigned(Ref) then
+    Exit(ECANVAS_NOCANVAS);
+  v := MemoryDataView;
+  v.setFloat32(aPointF,               Ref.Width,  env.IsLittleEndian);
+  v.setFloat32(aPointF + SizeFloat32, Ref.Height, env.IsLittleEndian);
+  Result := ECANVAS_SUCCESS;
+end;
+
+// SetCanvasSize
+//
+function TWasmFresnelWebApi.SetCanvasSize(const aID: TWindowCanvasID; aWidth, aHeight: TFresnelFloat): TCanvasError;
+var
+  Ref: TWindowReference;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.SetCanvasSizes(%d,%d,%d)', [aID,aWidth,aHeight]);
+  {$ENDIF}
+  Ref := GetWindowRef(aID);
+  if not Assigned(Ref) then
+    Exit(ECANVAS_NOCANVAS);
+  Ref.SetSize(aWidth, aHeight);
+  Result := ECANVAS_SUCCESS;
+end;
+
+// GetViewPortSizes
+//
+function TWasmFresnelWebApi.GetViewPortSizes(Flags: LongInt; aWidth, aHeight: PFresnelFloat): TCanvasError;
+var
+  W,H : NativeInt;
+  v : TJSDataView;
+begin
+  {$IFNDEF NOLOGAPICALLS}
+  if LogAPICalls then
+    LogCall('Canvas.GetViewPortSizes(%d,[%x],[%x])',[Flags,aWidth,aHeight]);
+  {$ENDIF}
+  if Flags=GETVIEWPORTSIZE_CLIENT then
+  begin
+    W:=document.documentElement.clientWidth;
+    H:=document.documentElement.clientHeight;
+  end
+  else if FLAGS=GETVIEWPORTSIZE_WINDOW then
+  begin
+    W:=Window.innerWidth;
+    H:=document.documentElement.clientHeight;
+  end
+  else
+  begin
+    W:=0;
+    H:=0;
+  end;
+  if (W=0) or (H=0) then
+    Result:=ECANVAS_INVALIDPARAM
+  else
+  begin
+    V:=MemoryDataView;
+    v.setFloat32(aWidth,  W, env.IsLittleEndian);
+    v.setFloat32(aHeight, H, env.IsLittleEndian);
+    Result:=ECANVAS_SUCCESS;
+  end;
+end;
+
+// HandleMenuClick
+//
+function TWasmFresnelWebApi.HandleMenuClick(aMenuID: TMenuID; aData: TWasmPointer): Boolean;
+var
+  lMessage : TFresnelMessage_HandleMenuClick;
+begin
+  lMessage := TFresnelMessage_HandleMenuClick.new;
+  lMessage.Typ := cFresnel_MenuClick;
+  lMessage.MenuID := aMenuID;
+  lMessage.UserData := aData;
+
+  Worker.postMessage(lMessage);
+end;
+
+// RequestAnimationFrame
+//
+procedure TWasmFresnelWebApi.RequestAnimationFrame(userdata: TWasmPointer);
+
+  procedure HandleAnimationFrame(timestamp{%H-} : Double);
+  var
+    lCallbackValue : JSValue;
+    lCallback : TAnimationFrameCallback absolute lCallbackValue;
+  begin
+    lCallbackValue := InstanceExports['__fresnel_animation_frame'];
+    if lCallbackValue then
+      lCallback;
+  end;
+
+begin
+  if LogAPICalls then
+    LogCall('FresnelApi.RequestAnimationFrame');
+  window.requestAnimationFrame(@HandleAnimationFrame);
+end;
+
+
+// UserMediaStartCapture
+//
+function TWasmFresnelWebApi.UserMediaStartCapture(
+  aDeviceID_UTF16: TWasmPointer; aDeviceID_UTF16Size: Integer;
+  aVideoID : TWasmPointer;
+  aResolutionWidth, aResolutionHeight : Integer;
+  aOptions : Integer
+  ) : TCanvasError;
+var
+  lVideoRef : TVideoReference;
+  lDeviceID : String;
+begin
+  lVideoRef := AllocateVideoReference;
+  try
+    lDeviceID := GetUTF16FromMem(aDeviceID_UTF16, aDeviceID_UTF16Size);
+    lVideoRef.StartCapture(
+      lDeviceID, 640, 480,
+      procedure (aTimeStamp : TJSDOMHighResTimeStamp; aSnapShot : TJSImageBitmap)
+      var
+        lCanvasRef : TOffscreenCanvasReference;
+        lCallback : JSValue;
+        lBitmapID : Integer;
+        lMessage : TFresnelMessage_UserMediaFrame;
+        lRafMsg : TFresnelMessage_RequestAnimationFrame;
+      begin
+        lMessage := TFresnelMessage_UserMediaFrame.new;
+        lMessage.Typ := cFresnel_UserMediaFrame;
+        lMessage.Timestamp := aTimeStamp;
+        lMessage.VideoID := lVideoRef.ID;
+        lMessage.ImageBitmap := aSnapShot;
+        Worker.postMessage(lMessage, [ aSnapShot ]);
+
+        if (aOptions and cUserMediaCaptureOption_RequestAnimationFrame) <> 0 then
+        begin
+          lRafMsg := TFresnelMessage_RequestAnimationFrame.new;
+          lRafMsg.Typ := cFresnel_RequestAnimationFrame;
+          DoWorkerMessage_RequestAnimationFrame(lRafMsg);
+        end;
+      end
+    );
+    MemoryDataView.setInt32(aVideoID, lVideoRef.ID, Env.IsLittleEndian);
+    Result := ECANVAS_SUCCESS;
+  except
+    on E: Exception do
+    begin
+      Writeln('In UserMediaStartCapture, ', E.ClassName, ': ', E.Message);
+      DeAllocateVideoElement(lVideoRef.ID);
+      Result := ECANVAS_EXCEPTION;
+    end;
+  end;
+end;
+
+// EnumerateUsermedia
+//
+function TWasmFresnelWebApi.EnumerateUsermedia(aUserData : TWasmPointer) : TCanvasError; // 'usermedia_enumerate';
+
+  procedure HandleUserMediaEnumerated;
+  var
+    lCallback : JSValue;
+  begin
+    lCallback := InstanceExports['__fresnel_usermedia_enumerated'];
+    if lCallback then
+      TUserMediaCallback(lCallback)(Length(FEnumeratedUserMedia), aUserData);
+  end;
+
+begin
+  DoEnumerateUserMedia(@HandleUserMediaEnumerated);
+  Result := ECANVAS_SUCCESS;
+end;
+
+// LocalStorageGetItem
+//
+function TWasmFresnelWebApi.LocalStorageGetItem(aKey: String; aDataBuffer: TJSSharedArrayBuffer): TCanvasError;
+var
+  lJSValue : JSValue;
+  lValue : String absolute lJSValue;
+  lInt32 : TJSInt32Array;
+  lUInt16 : TJSUInt16Array;
+  i : Integer;
+begin
+  lJSValue := window.localStorage.getItem(aKey);
+
+  lInt32 := TJSInt32Array.new(aDataBuffer, 0, 1);
+
+  if JS.isNull(lJSValue) then
+  begin
+    lInt32[0] := -1;
+    Exit(EWASMEVENT_SUCCESS);
+  end;
+
+  lInt32[0] := Length(lValue);
+  if aDataBuffer.byteLength < 2 * Length(lValue) + SizeInt32 then
+    Exit(EWASMEVENT_BUFFER_SIZE);
+
+  lUInt16 := TJSUInt16Array.new(aDataBuffer, 4, Length(lValue));
+  for i := 0 to Length(lValue)-1 do
+    lUInt16[i] := TJSString(lValue).charCodeAt(i);
+
+  Result := EWASMEVENT_SUCCESS;
+end;
+
+end.
+

+ 526 - 0
src/pas2js/fresnel.worker.pas2js.wasmapi.pp

@@ -0,0 +1,526 @@
+{
+    This file is part of the Fresnel Library.
+    Copyright (c) 2025 by the FPC & Lazarus teams.
+
+    Pas2js Fresnel interface - Webassembly rendering API
+
+    See the file COPYING.modifiedLGPL.txt, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+
+// hic sunt dracones
+
+{$mode objfpc}
+{$h+}
+{$modeswitch externalclass}
+{$modeswitch advancedrecords}
+
+{$DEFINE IMAGE_USEOSC}
+
+unit fresnel.worker.pas2js.wasmapi;
+
+interface
+
+uses
+  SysUtils, Types,
+  JS, WebOrWorker, WebWorker, wasienv,
+  Rtl.WorkerCommands,
+  fresnel.wasm.shared,
+  fresnel.messages.pas2js.wasmapi,
+  fresnel.menubuilder.pas2js.wasmapi,
+  fresnel.shared.pas2js;
+
+type
+
+  TMainThreadCallFunc = reference to function: TCanvasError;
+
+  TTimerCallback = procedure (aCurrentTicks, aPreviousTicks : NativeInt);
+
+  { TWasmFresnelWorkerApi }
+
+  TWasmFresnelWorkerApi = class(TWasmFresnelSharedApi)
+  private
+    FMenuSupport : Boolean;
+
+    FAtomicBuffer : TJSSharedArrayBuffer;
+    FAtomicArray : TJSInt32Array;
+
+    class var vLastPendingCallID : NativeInt;
+
+    // temporary to ignore some non-essential API calls
+    function CreateIgnoreCall(const aFuncName: String): TMainThreadCallFunc;
+
+    function CreateMainThreadBlockingCall(const aFuncName: String) : TMainThreadCallFunc;
+    function CreateMainThreadFireAndForgetCall(const aFuncName: String) : TMainThreadCallFunc;
+
+    procedure DeclareMainThreadBlockingCall(aImportObject : TJSObject; const aFuncName: String);
+    procedure DeclareMainThreadFireAndForgetCall(aImportObject : TJSObject; const aFuncName: String);
+    procedure DeclareMainThreadCall(aImportObject : TJSObject; const aFuncName: String);
+
+    function DrawOffscreenCanvasOnWindowCanvas(const aWindowID : TWindowCanvasID; const aCanvasID : TOffscreenCanvasID) : TCanvasError;
+
+    procedure DoTimerTick;
+
+
+  protected
+    procedure SetMenuSupport(const val : Boolean);
+
+    procedure SetupLocalStorageBridge;
+
+  public
+    constructor Create(aEnv : TPas2JSWASIEnvironment); override;
+
+    procedure HandleFresnelCommand(Cmd: TFresnelMessage);
+
+    procedure FillImportObject(aObject : TJSObject); override;
+
+    property MenuSupport : Boolean read FMenuSupport write SetMenuSupport;
+
+    // Menu
+
+    function HandleMenuClick(aMenuID : TMenuID; aData : TWasmPointer) : Boolean; override;
+
+    // RequestAnimationFrame
+
+    procedure RequestAnimationFrame(userData: TWasmPointer);
+    procedure DoHandleAnimationFrameMessage(aMessage : TFresnelMessage_RequestAnimationFrame);
+
+    // UserMedia
+
+    procedure EnumerateUserMedia(userData: TWasmPointer);
+    procedure DoHandleEnumeratedUserMediaMessage(aMessage : TFresnelMessage_EnumerateUserMedia);
+    procedure DoHandleUserMediaFrame(aMessage : TFresnelMessage_UserMediaFrame);
+
+  end;
+
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+implementation
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+// --------------------------------------------------------------------
+
+{ TWasmFresnelWorkerApi }
+
+// Create
+//
+constructor TWasmFresnelWorkerApi.Create(aEnv: TPas2JSWASIEnvironment);
+begin
+  inherited Create(aEnv);
+
+  FAtomicBuffer := TJSSharedArrayBuffer.new(8);
+  FAtomicArray := TJSInt32Array.new(FAtomicBuffer);
+  TCommandDispatcher.Instance.specialize AddCommandHandler<TFresnelMessage>(cmdFresnel,@HandleFresnelCommand);
+  SetupLocalStorageBridge;
+end;
+
+// CreateIgnoreCall
+//
+function TWasmFresnelWorkerApi.CreateIgnoreCall(const aFuncName: String): TMainThreadCallFunc;
+begin
+  Result := function () : TCanvasError
+  begin
+    writeln('Call ignored: ', aFuncName);
+  end;
+end;
+
+// CreateMainThreadBlockingCall
+//
+function Array_prototype_slice(val : JSValue) : TJSValueDynArray; external name 'Array.prototype.slice.call';
+
+function TWasmFresnelWorkerApi.CreateMainThreadBlockingCall(const aFuncName: String): TMainThreadCallFunc;
+begin
+  Result := function () : TCanvasError
+  var
+    pendingCallID : Integer;
+    lMessage : TFresnelMessage_FunctionCall;
+    atomicWaitOutcome : String;
+  begin
+    Inc(vLastPendingCallID);
+    pendingCallID := vLastPendingCallID;
+
+    lMessage := TFresnelMessage_FunctionCall.new;
+    lMessage.Typ := cFresnel_Message_Call;
+    lMessage.ID := pendingCallID;
+    lMessage.FuncName := aFuncName;
+    lMessage.Args := Array_prototype_slice(JSArguments);
+    lMessage.Memory := Env.Memory.buffer;
+    lMessage.Atomic := FAtomicArray;
+
+    TJSAtomics.store(FAtomicArray, 0, 0);
+
+    TJSDedicatedWorkerGlobalScope(Self_).postMessage(lMessage);
+
+    atomicWaitOutcome := TJSAtomics.wait(FAtomicArray, 0, 0);
+    //Console.log('Atomics wait for pendingCallID ', pendingCallID, '  = ', atomicWaitOutcome);
+
+    Result := TJSAtomics.load(FAtomicArray, 1);
+  end;
+end;
+
+// CreateMainThreadFireAndForgetCall
+//
+function TWasmFresnelWorkerApi.CreateMainThreadFireAndForgetCall(const aFuncName: String): TMainThreadCallFunc;
+begin
+  Result := function () : TCanvasError
+  var
+    pendingCallID : Integer;
+    lMessage : TFresnelMessage_FunctionCall;
+  begin
+    Inc(vLastPendingCallID);
+    pendingCallID := vLastPendingCallID;
+
+    lMessage := TFresnelMessage_FunctionCall.new;
+    lMessage.Typ := cFresnel_Message_Call;
+    lMessage.ID := pendingCallID;
+    lMessage.FuncName := aFuncName;
+    lMessage.Args := Array_prototype_slice(JSArguments);
+
+    TJSDedicatedWorkerGlobalScope(Self_).postMessage(lMessage);
+
+    Result := ECANVAS_SUCCESS;
+  end;
+end;
+
+// DeclareMainThreadBlockingCall
+//
+procedure TWasmFresnelWorkerApi.DeclareMainThreadBlockingCall(aImportObject: TJSObject; const aFuncName: String);
+begin
+  aImportObject[aFuncName] := CreateMainThreadBlockingCall(aFuncName);
+end;
+
+// DeclareMainThreadFireAndForgetCall
+//
+procedure TWasmFresnelWorkerApi.DeclareMainThreadFireAndForgetCall(aImportObject: TJSObject; const aFuncName: String);
+begin
+  aImportObject[aFuncName] := CreateMainThreadFireAndForgetCall(aFuncName);
+end;
+
+// DeclareMainThreadCall
+//
+procedure TWasmFresnelWorkerApi.DeclareMainThreadCall(aImportObject: TJSObject; const aFuncName: String);
+begin
+  DeclareMainThreadBlockingCall(aImportObject, aFuncName);
+end;
+
+// HandleFresnelCommand
+//
+procedure TWasmFresnelWorkerApi.HandleFresnelCommand(Cmd : TFresnelMessage);
+var
+  dataType: String;
+begin
+  dataType := cmd.Typ;
+  Case dataType of
+  cFresnel_RequestAnimationFrame:
+    DoHandleAnimationFrameMessage(TFresnelMessage_RequestAnimationFrame(cmd));
+  cFresnel_EnqueueEvent:
+    EnqueueEvent(TWindowEvent(TFresnelMessage_EnqueueEvent(cmd).Event));
+  cFresnel_Tick:
+    DoTimerTick;
+  cFresnel_Message_Call:
+    begin
+    if (cmd['funcName'] = 'wake_main_thread') then
+      MainThreadWake;
+    end;
+  cFresnel_UserMediaFrame:
+    DoHandleUserMediaFrame(TFresnelMessage_UserMediaFrame(cmd));
+  cFresnel_MenuClick:
+    HandleMenuClick(
+      TFresnelMessage_HandleMenuClick(cmd).MenuID,
+      TFresnelMessage_HandleMenuClick(cmd).UserData
+    );
+  cFresnel_EnumerateUserMedia:
+    DoHandleEnumeratedUserMediaMessage(TFresnelMessage_EnumerateUserMedia(cmd));
+  else
+    writeln('Unsupported Fresnel message type ', TJSJSON.stringify(cmd));
+  end;
+end;
+
+// RequestAnimationFrame
+//
+procedure TWasmFresnelWorkerApi.RequestAnimationFrame(userData: TWasmPointer);
+var
+  lMessage: TFresnelMessage_RequestAnimationFrame;
+begin
+  lMessage := TFresnelMessage_RequestAnimationFrame(TFresnelMessage.newMessage(cFresnel_RequestAnimationFrame));
+  lMessage.UserData := userData;
+  TCommandDispatcher.Instance.SendCommand(lMessage);
+end;
+
+// DoHandleAnimationFrameMessage
+//
+procedure TWasmFresnelWorkerApi.DoHandleAnimationFrameMessage(aMessage : TFresnelMessage_RequestAnimationFrame);
+var
+  lCallbackValue : JSValue;
+  lCallback : TAnimationFrameCallback absolute lCallbackValue;
+begin
+  lCallbackValue := InstanceExports['__fresnel_animation_frame'];
+  if lCallbackValue then
+    lCallback;
+end;
+
+// EnumerateUserMedia
+//
+procedure TWasmFresnelWorkerApi.EnumerateUserMedia(userData: TWasmPointer);
+var
+  lMessage: TFresnelMessage_EnumerateUserMedia;
+begin
+  lMessage := TFresnelMessage_EnumerateUserMedia(TFresnelMessage.NewMessage(cFresnel_EnumerateUserMedia));
+  lMessage.UserData := userData;
+  TCommandDispatcher.Instance.SendCommand(lMessage);
+end;
+
+// DoHandleEnumeratedUserMediaMessage
+//
+procedure TWasmFresnelWorkerApi.DoHandleEnumeratedUserMediaMessage(aMessage: TFresnelMessage_EnumerateUserMedia);
+var
+  lCallback : JSValue;
+begin
+  FEnumeratedUserMedia := aMessage.UserMediaData;
+
+  lCallback := InstanceExports['__fresnel_usermedia_enumerated'];
+  if lCallback then
+    TUserMediaCallback(lCallback)(Length(FEnumeratedUserMedia), aMessage.UserData);
+end;
+
+// DoHandleUserMediaFrame
+//
+procedure TWasmFresnelWorkerApi.DoHandleUserMediaFrame(aMessage: TFresnelMessage_UserMediaFrame);
+var
+  lBitmapID : Integer;
+  lCallback : JSValue;
+begin
+  lCallback := InstanceExports['__fresnel_usermedia_frame'];
+  if lCallback then
+  begin
+    lBitmapID := StoreImageBitmap(aMessage.ImageBitmap);
+    TUserMediaFrameCallback(lCallback)(aMessage.Timestamp, aMessage.VideoID, lBitmapID);
+  end
+  else aMessage.ImageBitmap.close;
+end;
+
+// DrawOffscreenCanvasOnWindowCanvas
+//
+function TWasmFresnelWorkerApi.DrawOffscreenCanvasOnWindowCanvas(
+ const aWindowID: TWindowCanvasID; const aCanvasID: TOffscreenCanvasID) : TCanvasError;
+var
+  lMessage : TFresnelMessage_DrawOffscreenCanvasOnWindow;
+  pendingCallID : Integer;
+  canvasRef : TOffscreenCanvasReference;
+  canvas : TJSHTMLOffscreenCanvas;
+begin
+  Inc(vLastPendingCallID);
+  pendingCallID := vLastPendingCallID;
+
+  canvasRef := GetOffscreenCanvasRef(aCanvasID);
+  if canvasRef = nil then
+    Exit(ECANVAS_NOCANVAS);
+
+  canvas := canvasRef.Canvas;
+
+  if (canvas.Width = 0) or (canvas.Height = 0) then
+    Exit(ECANVAS_SUCCESS);
+
+  lMessage := TFresnelMessage_DrawOffscreenCanvasOnWindow(TFresnelMessage.NewMessage(cFresnel_Message_DOCOW));
+  lMessage.ID := pendingCallID;
+  lMessage.Atomic := FAtomicArray;
+  lMessage.WindowID := aWindowID;
+  lMessage.ImageBitmap := canvas.transferToImageBitmap;
+
+  TJSAtomics.store(FAtomicArray, 0, 0);
+
+  TCommandDispatcher.Instance.SendCommand(lMessage, [ lMessage.ImageBitmap ]);
+
+  TJSAtomics.wait(FAtomicArray, 0, 0);
+
+  Result := TJSAtomics.load(FAtomicArray, 1);
+end;
+
+// DoTimerTick
+//
+var
+  vLastTimeTimerTick : NativeInt;
+procedure TWasmFresnelWorkerApi.DoTimerTick;
+var
+  Callback : JSValue;
+  T : NativeInt;
+begin
+  T := vLastTimeTimerTick;
+  vLastTimeTimerTick := TJSDate.now;
+  if not assigned(InstanceExports) then
+    Console.log('DoTimerTick: no instance exports !')
+  else
+  begin
+    Callback := InstanceExports['__fresnel_tick'];
+    if Assigned(Callback) then
+      TTimerCallback(CallBack)(vLastTimeTimerTick, T)
+    else
+      Console.warn('DoTimerTick: no tick callback !');
+  end;
+end;
+
+
+// SetMenuSupport
+//
+procedure TWasmFresnelWorkerApi.SetMenuSupport(const val: Boolean);
+var
+  lMessage : TFresnelMessage;
+begin
+  if val = FMenuSupport then
+    Exit;
+
+  FMenuSupport := val;
+  lMessage := TFresnelMessages.CreateMessage_MenuSupport(val);
+  TCommandDispatcher.Instance.SendCommand(lMessage);
+end;
+
+// SetupLocalStorageBridge
+//
+procedure TWasmFresnelWorkerApi.SetupLocalStorageBridge;
+
+type
+  TGetItemMainThread = reference to function (aName : String; aDataBuffer : TJSSharedArrayBuffer) : TCanvasError;
+
+var
+  setItem, getItem, removeItem, clear : TMainThreadCallFunc;
+  getItemMain : TGetItemMainThread;
+
+begin
+  setItem := CreateMainThreadBlockingCall('localstorage_setitem');
+  removeItem := CreateMainThreadBlockingCall('localstorage_removeitem');
+  clear :=  CreateMainThreadBlockingCall('localstorage_clear');
+
+  getItemMain := TGetItemMainThread(CreateMainThreadBlockingCall('localstorage_getitem'));
+  getItem := TMainThreadCallFunc(
+    function (aName : String) : JSValue
+    var
+      lDataBuffer : TJSSharedArrayBuffer;
+      lStringArray : TJSUInt16Array;
+      lErr : TCanvasError;
+      lAttempts, lReturnedSize : Integer;
+    begin
+      lDataBuffer := TJSSharedArrayBuffer.new(16*1024); // initial guess of 16 kb
+      lAttempts := 3;
+      while lAttempts > 0 do
+      begin
+        lErr := getItemMain(aName, lDataBuffer);
+        case lErr of
+          EWASMEVENT_SUCCESS : begin
+            lReturnedSize := TJSInt32Array.new(lDataBuffer, 0, 1)[0];
+            if lReturnedSize = -1 then
+              Exit(JS.Undefined);
+            lStringArray := TJSUint16Array.new(lDataBuffer, 4, lReturnedSize);
+            Result := TJSFunction(@TJSString.fromCharCode).apply(nil, TJSValueDynArray(lStringArray));
+            Exit;
+          end;
+          EWASMEVENT_BUFFER_SIZE : begin
+            lReturnedSize := TJSInt32Array.new(lDataBuffer, 0, 1)[0];
+            lDataBuffer := TJSSharedArrayBuffer.new(lReturnedSize*2 + 1024); // adjust size with extra 1 kb margin
+          end;
+        else
+          raise Exception.Create('LocalStorageBridge Error ' + IntToStr(lErr));
+        end;
+        Dec(lAttempts);
+      end;
+      raise Exception.Create('LocalStorageBridge failed after multiple attempts');
+    end
+  );
+
+  asm
+    self.localStorage = { getItem, setItem, removeItem, clear };
+  end;
+end;
+
+// FillImportObject
+//
+procedure TWasmFresnelWorkerApi.FillImportObject(aObject: TJSObject);
+begin
+  inherited FillImportObject(aObject);
+
+  // Window
+
+  DeclareMainThreadCall(aObject, 'canvas_allocate_window');
+  DeclareMainThreadCall(aObject, 'canvas_deallocate_window');
+
+  DeclareMainThreadCall(aObject, 'window_show_hide');
+
+  aObject['canvas_draw_offscreen_on_window'] := @DrawOffscreenCanvasOnWindowCanvas;
+
+  DeclareMainThreadCall(aObject, 'canvas_getrect');
+  DeclareMainThreadCall(aObject, 'canvas_setrect');
+  DeclareMainThreadCall(aObject, 'canvas_getsize');
+  DeclareMainThreadCall(aObject, 'canvas_setsize');
+
+  DeclareMainThreadCall(aObject, 'canvas_set_title');
+
+  DeclareMainThreadCall(aObject, 'canvas_get_viewport_sizes');
+
+  // Cursor
+
+  DeclareMainThreadCall(aObject, 'cursor_set');
+
+  // RequestAnimationFrame
+
+  aObject['request_animation_frame'] := @RequestAnimationFrame;
+
+  // Clipboard
+
+  DeclareMainThreadCall(aObject, 'clipboard_read_text');
+  DeclareMainThreadCall(aObject, 'clipboard_write_text');
+
+  // Event
+
+  aObject['event_set_special_keymap'] := CreateIgnoreCall('event_set_special_keymap');
+  DeclareMainThreadFireAndForgetCall(aObject, 'wake_main_thread');
+
+  // Menu
+
+  DeclareMainThreadCall(aObject, 'menu_add_item');
+  DeclareMainThreadCall(aObject, 'menu_remove_item');
+  DeclareMainThreadCall(aObject, 'menu_update_item');
+
+  // Debug
+
+  aObject['console_log'] := @ConsoleLog;
+
+  // UserMedia
+
+  aObject['usermedia_enumerate'] := @EnumerateUserMedia;
+  aObject['usermedia_getenumerated'] := @GetEnumeratedUserMedia;
+
+  DeclareMainThreadCall(aObject, 'usermedia_startcapture');
+  DeclareMainThreadCall(aObject, 'usermedia_stopcapture');
+  DeclareMainThreadCall(aObject, 'usermedia_iscapturing');
+
+end;
+
+// HandleMenuClick
+//
+function TWasmFresnelWorkerApi.HandleMenuClick(aMenuID: TMenuID; aData: TWasmPointer): Boolean;
+var
+  Callback : JSValue;
+begin
+  Result:=False;
+  if not assigned(InstanceExports) then
+    Console.warn('No instance exports !')
+  else
+  begin
+    Callback:=InstanceExports['__fresnel_menu_click'];
+    if Assigned(Callback) then
+    begin
+      TMenuClickCallback(CallBack)(aMenuID,AData);
+      Result:=True;
+    end
+    else
+      Console.warn('No menu click callback !');
+  end;
+end;
+
+end.
+

+ 119 - 0
src/pas2js/importextensionex.pas2js.wasmapi.pp

@@ -0,0 +1,119 @@
+{
+    This file is part of the Fresnel Library.
+    Copyright (c) 2025 by the FPC & Lazarus teams.
+
+    Pas2js Fresnel interface - Webassembly rendering API
+
+    See the file COPYING.modifiedLGPL.txt, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+unit importextensionex.pas2js.wasmapi;
+
+{$mode ObjFPC}
+
+interface
+
+uses
+  JS, WebOrWorker, wasienv, webassembly;
+
+type
+
+  (*
+    TImportExtension extended class that introduces
+
+    - mechanism for cahing the DataView, to be extended with memory growth notification
+    - methods to get/set utf16 strings from wasm memory without utf8 roundtrips
+  *)
+
+  { TImportExtensionEx }
+
+  TImportExtensionEx = class (TImportExtension)
+  private
+    FWebAssemblyMemory : TJSArrayBuffer;
+    FMemoryDataView : TJSDataView;
+    FEnvMemoryDataView : TJSDataView;
+
+  protected
+    function GetUTF16FromMem(p : TWasmPointer; sizeInChars : Integer) : String;
+    procedure SetUTF16ToMem(p : TWasmPointer; const text : String);
+
+    procedure SetWebAssemblyMemory(aMemory : TJSArrayBuffer);
+    function GetModuleMemoryDataView : TJSDataView; reintroduce; deprecated 'Use MemoryDataView';
+    function GetMemoryDataView : TJSDataView;
+
+
+  public
+    property MemoryDataView : TJSDataView read GetMemoryDataView;
+    property WebAssemblyMemory : TJSArrayBuffer read FWebAssemblyMemory write SetWebAssemblyMemory;
+
+  end;
+
+implementation
+
+{ TImportExtensionEx }
+
+// GetUTF16FromMem
+//
+function TImportExtensionEx.GetUTF16FromMem(p: TWasmPointer; sizeInChars: Integer): String;
+
+begin
+  Result:=Env.GetUTF16StringFromMem(P,SizeInChars);
+end;
+
+// SetUTF16ToMem
+//
+procedure TImportExtensionEx.SetUTF16ToMem(p: TWasmPointer; const text: String);
+begin
+  Env.SetUTF16StringInMem(P,Text);
+end;
+
+// SetWebAssemblyMemory
+//
+procedure TImportExtensionEx.SetWebAssemblyMemory(aMemory: TJSArrayBuffer);
+begin
+  if FWebAssemblyMemory = aMemory then
+    Exit;
+
+  FWebAssemblyMemory := aMemory;
+  if aMemory <> nil then
+    FMemoryDataView := TJSDataView.New(aMemory)
+  else FMemoryDataView := nil;
+end;
+
+// GetModuleMemoryDataView
+//
+function TImportExtensionEx.GetModuleMemoryDataView: TJSDataView;
+begin
+  Result := inherited getModuleMemoryDataView;
+end;
+
+// GetMemoryDataView
+//
+function TImportExtensionEx.GetMemoryDataView: TJSDataView;
+var
+  envMemory : JSValue;
+  envWebAssemblyMemory : TJSWebAssemblyMemory absolute envMemory;
+begin
+  if FWebAssemblyMemory = nil then
+  begin
+    envMemory := InstanceExports.Memory;
+    if envMemory then
+    begin
+      if (FEnvMemoryDataView = nil) or (FEnvMemoryDataView.buffer <> envWebAssemblyMemory.buffer) then
+        FEnvMemoryDataView := TJSDataView.New(envWebAssemblyMemory.buffer);
+      Exit(FEnvMemoryDataView);
+    end
+    else
+      Console.error('Memory not accessible from this call chain');
+  end;
+
+  Result := FMemoryDataView;
+end;
+
+end.
+

+ 36 - 4
src/pas2js/p2jsfresnelapi.lpk

@@ -17,10 +17,6 @@
     </CompilerOptions>
     </CompilerOptions>
     <Version Major="1"/>
     <Version Major="1"/>
     <Files>
     <Files>
-      <Item>
-        <Filename Value="fresnel.pas2js.wasmapi.pp"/>
-        <UnitName Value="fresnel.pas2js.wasmapi"/>
-      </Item>
       <Item>
       <Item>
         <Filename Value="../wasm/fresnel.wasm.shared.pp"/>
         <Filename Value="../wasm/fresnel.wasm.shared.pp"/>
         <UnitName Value="fresnel.wasm.shared"/>
         <UnitName Value="fresnel.wasm.shared"/>
@@ -29,6 +25,42 @@
         <Filename Value="../base/fresnel.keys.pas"/>
         <Filename Value="../base/fresnel.keys.pas"/>
         <UnitName Value="fresnel.keys"/>
         <UnitName Value="fresnel.keys"/>
       </Item>
       </Item>
+      <Item>
+        <Filename Value="fresnel.browser.pas2js.wasmapi.pp"/>
+        <UnitName Value="fresnel.browser.pas2js.wasmapi"/>
+      </Item>
+      <Item>
+        <Filename Value="fresnel.menubuilder.pas2js.wasmapi.pp"/>
+        <UnitName Value="fresnel.menubuilder.pas2js.wasmapi"/>
+      </Item>
+      <Item>
+        <Filename Value="fresnel.messages.pas2js.wasmapi.pp"/>
+        <UnitName Value="fresnel.messages.pas2js.wasmapi"/>
+      </Item>
+      <Item>
+        <Filename Value="fresnel.selfiesegmentation.pas2js.pp"/>
+        <UnitName Value="fresnel.selfiesegmentation.pas2js"/>
+      </Item>
+      <Item>
+        <Filename Value="fresnel.shared.pas2js.pp"/>
+        <UnitName Value="fresnel.shared.pas2js"/>
+      </Item>
+      <Item>
+        <Filename Value="fresnel.simple.pas2js.wasmapi.pp"/>
+        <UnitName Value="fresnel.simple.pas2js.wasmapi"/>
+      </Item>
+      <Item>
+        <Filename Value="fresnel.web.pas2js.wasmapi.pp"/>
+        <UnitName Value="fresnel.web.pas2js.wasmapi"/>
+      </Item>
+      <Item>
+        <Filename Value="fresnel.worker.pas2js.wasmapi.pp"/>
+        <UnitName Value="fresnel.worker.pas2js.wasmapi"/>
+      </Item>
+      <Item>
+        <Filename Value="importextensionex.pas2js.wasmapi.pp"/>
+        <UnitName Value="importextensionex.pas2js.wasmapi"/>
+      </Item>
     </Files>
     </Files>
     <UsageOptions>
     <UsageOptions>
       <UnitPath Value="$(PkgDir);$(PkgDir)/../wasm;$(PkgDir)/../base"/>
       <UnitPath Value="$(PkgDir);$(PkgDir)/../wasm;$(PkgDir)/../base"/>

+ 4 - 2
src/pas2js/p2jsfresnelapi.pas

@@ -2,13 +2,15 @@
   This source is only used to compile and install the package.
   This source is only used to compile and install the package.
  }
  }
 
 
-unit p2jsfresnelapi;
+unit P2jsFresnelAPI;
 
 
 {$warn 5023 off : no warning about unused units}
 {$warn 5023 off : no warning about unused units}
 interface
 interface
 
 
 uses
 uses
-  fresnel.pas2js.wasmapi, fresnel.wasm.shared, fresnel.keys;
+  fresnel.wasm.shared, fresnel.keys, fresnel.browser.pas2js.wasmapi, fresnel.menubuilder.pas2js.wasmapi, 
+  fresnel.messages.pas2js.wasmapi, fresnel.selfiesegmentation.pas2js, fresnel.shared.pas2js, fresnel.simple.pas2js.wasmapi, 
+  fresnel.web.pas2js.wasmapi, fresnel.worker.pas2js.wasmapi, importextensionex.pas2js.wasmapi;
 
 
 implementation
 implementation