diff --git a/docs/docs/lua.md b/docs/docs/lua.md new file mode 100644 index 0000000..ecc8369 --- /dev/null +++ b/docs/docs/lua.md @@ -0,0 +1,1267 @@ +--- +sidebar_position: 3 +--- + +# Lua API + +Gurted provides a Lua API that enables dynamic web development with client-side scripting. The Lua runtime is integrated into the browser engine and provides access to DOM manipulation, network requests, animations, and more. + +## Global: gurt + +The main global object for DOM manipulation and core functionality. + +### gurt.select(selector) + +Selects the first element matching the CSS selector. + +```lua +local element = gurt.select('#my-id') +local firstButton = gurt.select('button') +local classElement = gurt.select('.my-class') +``` + +### gurt.selectAll(selector) + +Selects all elements matching the CSS selector, returns an array. + +```lua +local allButtons = gurt.selectAll('button') +local listItems = gurt.selectAll('li') + +-- Iterate through results +for i = 1, #allButtons do + local button = allButtons[i] + button.text = 'Button ' .. i +end +``` + +### gurt.create(tagName, options) + +Creates a new HTML element. + +```lua +-- Basic element +local div = gurt.create('div') + +-- Element with attributes and content +local button = gurt.create('button', { + text = 'Click me!', + style = 'bg-blue-500 text-white px-4 py-2 rounded', + id = 'my-button' +}) +``` + +### gurt.body + +Reference to the document body element. + +```lua +-- Add event listeners to body +gurt.body:on('keydown', function(event) + trace.log('Key pressed: ' .. event.key) +end) + +-- Append elements to body +local newDiv = gurt.create('div', { text = 'Hello World!' }) +gurt.body:append(newDiv) +``` + +### gurt.location + +Browser location and navigation control. + +### gurt.location.href + +Gets the current URL. + +```lua +local currentUrl = gurt.location.href +trace.log('Current URL: ' .. currentUrl) +``` + +### gurt.location.reload() + +Reloads the current page. + +```lua +gurt.location.reload() +``` + +### gurt.location.goto(url) + +Navigates to a new URL. + +```lua +gurt.location.goto('gurt://example.com/page') +gurt.location.goto('https://external-site.com') +``` + +### gurt.location.query + +Query parameter access. + +```lua +-- Get a specific parameter +local userId = gurt.location.query.get('user_id') + +-- Check if parameter exists +if gurt.location.query.has('debug') then + trace.log('Debug mode enabled') +end + +-- Get all values for a parameter (for repeated params) +local tags = gurt.location.query.getAll('tag') +``` +## Global: trace + +The global trace table for logging messages to the console. + +### trace.log(message) +Identical to `print()`, logs a message to the console. + +```lua +trace.log('Hello from Lua!') +``` + +### trace.warn(message) +Logs a warning message to the console. + +```lua +trace.warn('This is a warning!') +``` + +### trace.error(message) +Logs an error message to the console. + +```lua +trace.error('This is an error!') +``` + +## Element + +Elements returned by `gurt.select()`, `gurt.create()`, etc. have the following properties and methods: + +### Properties + +#### element.text + +Gets or sets the text content of an element. + +```lua +local p = gurt.select('p') +p.text = 'New paragraph content' +local currentText = p.text +``` + +#### element.value + +Gets or sets the value of form elements. + +```lua +local input = gurt.select('#username') +input.value = 'john_doe' +local username = input.value + +local checkbox = gurt.select('#agree') +checkbox.value = true -- Check the checkbox +``` + +#### element.visible + +Gets or sets element visibility. + +```lua +local modal = gurt.select('#modal') +modal.visible = false -- Hide element +modal.visible = true -- Show element + +if modal.visible then + trace.log('Element is visible') +end +``` + +#### element.children + +Gets an array of child elements. + +```lua +local container = gurt.select('.container') +local children = container.children + +for i = 1, #children do + local child = children[i] + trace.log('Child ' .. i .. ': ' .. child.text) +end +``` + +### DOM Traversal + +#### element.parent + +Gets the parent element. + +```lua +local button = gurt.select('#my-button') +local container = button.parent +``` + +#### element.nextSibling / element.previousSibling + +Gets adjacent sibling elements. + +```lua +local current = gurt.select('#current-item') +local next = current.nextSibling +local prev = current.previousSibling +``` + +#### element.firstChild / element.lastChild + +Gets first or last child element. + +```lua +local list = gurt.select('ul') +local firstItem = list.firstChild +local lastItem = list.lastChild +``` + +### Methods + +#### element:on(eventName, callback) + +Adds an event listener. Returns a subscription object. + +```lua +local button = gurt.select('#my-button') + +-- Click event +local subscription = button:on('click', function() + trace.log('Button clicked!') +end) + +-- Mouse events +button:on('mouseenter', function() + button.classList:add('hover-effect') +end) + +button:on('mouseexit', function() + button.classList:remove('hover-effect') +end) + +-- Input events (for form elements) +local input = gurt.select('#username') +input:on('change', function(event) + trace.log('Input changed to: ' .. event.value) +end) + +-- Focus events +input:on('focusin', function() + trace.log('Input focused') +end) + +input:on('focusout', function() + trace.log('Input lost focus') +end) + +-- Unsubscribe from event +subscription:unsubscribe() +``` + +#### element:append(childElement) + +Adds a child element. + +```lua +local container = gurt.select('.container') +local newDiv = gurt.create('div', { text = 'New content' }) +container:append(newDiv) +``` + +#### element:remove() + +Removes the element from the DOM. + +```lua +local elementToRemove = gurt.select('#temporary') +elementToRemove:remove() +``` + +#### element:insertBefore(newElement, referenceElement) + +Inserts an element before another element. + +```lua +local container = gurt.select('.container') +local newElement = gurt.create('div', { text = 'Inserted' }) +local reference = gurt.select('#reference') +container:insertBefore(newElement, reference) +``` + +#### element:insertAfter(newElement, referenceElement) + +Inserts an element after another element. + +```lua +local container = gurt.select('.container') +local newElement = gurt.create('div', { text = 'Inserted' }) +local reference = gurt.select('#reference') +container:insertAfter(newElement, reference) +``` + +#### element:replace(oldElement, newElement) + +Replaces a child element with a new element. + +```lua +local container = gurt.select('.container') +local oldElement = gurt.select('#old') +local newElement = gurt.create('div', { text = 'Replacement' }) +container:replace(oldElement, newElement) +``` + +#### element:clone(deep) + +Creates a copy of the element. + +```lua +-- Shallow clone (element only) +local copy = element:clone(false) + +-- Deep clone (element and all children) +local deepCopy = element:clone(true) +``` + +#### element:getAttribute(name) / element:setAttribute(name, value) + +Gets or sets element attributes. + +```lua +local img = gurt.select('img') +local src = img:getAttribute('src') +img:setAttribute('alt', 'Description text') + +-- Remove attribute by setting empty value +img:setAttribute('title', '') +``` + +#### element:show() / element:hide() + +Shows or hides an element. + +```lua +local modal = gurt.select('#modal') +modal:show() -- Makes element visible +modal:hide() -- Hides element +``` + +#### element:focus() / element:unfocus() + +Sets or removes focus from an element. + +```lua +local input = gurt.select('#search') +input:focus() -- Focus the input +input:unfocus() -- Remove focus +``` + +### Class List Management + +#### element.classList + +Provides methods for managing CSS classes. + +```lua +local button = gurt.select('#my-button') + +-- Add classes +button.classList:add('active') +button.classList:add('btn-primary') + +-- Remove classes +button.classList:remove('disabled') + +-- Toggle classes +button.classList:toggle('selected') + +-- Check if class exists +if button.classList:contains('active') then + trace.log('Button is active') +end + +-- Get specific class by index (1-based) +local firstClass = button.classList:item(1) + +-- Get number of classes +local classCount = button.classList.length +``` + +### Animations + +#### element:createTween() + +Creates a tween animation for the element. + +```lua +local box = gurt.select('#animated-box') + +-- Fade out +box:createTween() + :to('opacity', 0) + :duration(1.0) + :easing('out') + :transition('linear') + :play() + +-- Move and scale +box:createTween() + :to('x', 200) + :to('y', 100) + :to('scale', 1.5) + :duration(2.0) + :easing('inout') + :transition('cubic') + :play() + +-- Color animation +box:createTween() + :to('backgroundColor', '#ff0000') + :duration(1.5) + :easing('out') + :transition('quad') + :play() + +-- Rotation +box:createTween() + :to('rotation', 360) + :duration(3.0) + :easing('inout') + :transition('sine') + :play() +``` + +**Available Tween Properties:** +- `opacity` - Element transparency (0-1) +- `backgroundColor` - Background color (hex format) +- `scale` - Element scale (1.0 = normal size) +- `rotation` - Rotation in degrees +- `x`, `y` - Position offset + +**Easing Types:** `'in'`, `'out'`, `'inout'`, `'outin'` + +**Transition Types:** `'linear'`, `'quad'`, `'cubic'`, `'quart'`, `'quint'`, `'sine'`, `'expo'`, `'circ'`, `'elastic'`, `'back'`, `'bounce'` + +![CRT effect](../static/img/docs/tween.png) +Resource: [Reddit](https://www.reddit.com/r/godot/comments/frqzup/godot_tweening_cheat_sheet/) + +## Audio API + +Work with audio elements for sound playback. + +```lua +local audio = gurt.select('#my-audio') + +audio:play() -- Start playback +audio:pause() -- Pause playback +audio:stop() -- Stop and reset + +audio.currentTime = 30.0 -- Seek to 30 seconds +audio.volume = 0.8 -- Set volume (0.0 - 1.0) +audio.loop = true -- Enable looping +audio.src = 'gurt://new-audio.mp3' -- Change source + +local duration = audio.duration +local currentPos = audio.currentTime +local isPlaying = audio.playing +local isPaused = audio.paused +``` + +## Canvas API + +Gurted features a 2D canvas API similar to HTML5 Canvas, plus shader support. + +### Context + +```lua +local canvas = gurt.select('#my-canvas') + +local ctx = canvas:withContext('2d') +local shaderCtx = canvas:withContext('shader') +``` + +### 2D Drawing Context + +#### Rectangle + +```lua +-- Fill a solid rectangle +ctx:fillRect(x, y, width, height, color) +ctx:fillRect(50, 50, 100, 75, '#ff0000') -- Red filled rectangle + +-- Draw rectangle outline +ctx:strokeRect(x, y, width, height, color, strokeWidth) +ctx:strokeRect(200, 50, 100, 75, '#00ff00', 3) -- Green outline, 3px thick + +-- Clear a rectangular area +ctx:clearRect(x, y, width, height) +ctx:clearRect(80, 80, 40, 40) -- Clear 40x40 area +``` + +#### Circle + +```lua +-- Draw filled or outlined circles +ctx:drawCircle(x, y, radius, color, filled) +ctx:drawCircle(150, 100, 30, '#0000ff', true) -- Filled blue circle +ctx:drawCircle(200, 100, 30, '#ff00ff', false) -- Outlined magenta circle +``` + +#### Text + +```lua +ctx:drawText(x, y, text, color) +ctx:drawText(20, 250, 'Hello Canvas!', '#ffffff') +ctx:drawText(20, 280, 'Default Font Only', '#ffff00') + +-- Font size can be set with setFont (size only, not family) +ctx:setFont('20px sans-serif') -- Only size matters +ctx:drawText(20, 300, 'Larger text', '#00ff00') + +local metrics = ctx:measureText('Sample Text') +local textWidth = metrics.width +``` + +### Path-Based Drawing + +For complex shapes, use path-based drawing methods: + +```lua +ctx:beginPath() + +-- Move to starting point without drawing +ctx:moveTo(100, 100) + +-- Draw line to point +ctx:lineTo(200, 150) +ctx:lineTo(150, 200) +ctx:lineTo(50, 200) + +-- Close the path (connects back to start) +ctx:closePath() + +-- Draw the path +ctx:stroke() -- Draw outline +-- or +ctx:fill() -- Fill the shape +``` + +#### Advanced Path Methods + +##### Arc and Circle Paths + +```lua +-- Draw arc (part of circle) +ctx:arc(x, y, radius, startAngle, endAngle, counterclockwise) + +-- Example: Draw a quarter circle +ctx:beginPath() +ctx:arc(200, 200, 50, 0, math.pi/2, false) -- 0 to 90 degrees +ctx:stroke() + +-- Full circle path +ctx:beginPath() +ctx:arc(300, 200, 40, 0, 2 * math.pi, false) -- 0 to 360 degrees +ctx:fill() +``` + +##### Curve Methods + +```lua +-- Quadratic curve (one control point) +ctx:quadraticCurveTo(controlX, controlY, endX, endY) + +-- Example: Smooth curve +ctx:beginPath() +ctx:moveTo(50, 300) +ctx:quadraticCurveTo(150, 250, 250, 300) -- Control point at (150,250) +ctx:stroke() + +-- Bezier curve (two control points) +ctx:bezierCurveTo(cp1x, cp1y, cp2x, cp2y, endX, endY) + +-- Example: S-curve +ctx:beginPath() +ctx:moveTo(50, 350) +ctx:bezierCurveTo(100, 300, 200, 400, 250, 350) +ctx:stroke() +``` + +#### Styling and Properties + +##### Setting Draw Styles + +```lua +-- Set stroke (outline) color +ctx:setStrokeStyle('#ff0000') -- Red outline +ctx:setStrokeStyle('rgba(255, 0, 0, 0.5)') -- Semi-transparent red +ctx:setStrokeStyle('red-500') -- Tailwind color names +ctx:setStrokeStyle('blue') -- Named colors + +-- Set fill color +ctx:setFillStyle('#00ff00') -- Green fill +ctx:setFillStyle('#33aa88') -- Teal fill +ctx:setFillStyle('slate-800') -- Tailwind colors +ctx:setFillStyle('transparent') -- Named transparent + +-- Set line width for strokes +ctx:setLineWidth(5) -- 5 pixel wide lines +ctx:setLineWidth(0.5) -- Thin lines + +-- Set font for text (size only, not family) +ctx:setFont('20px sans-serif') -- Only size matters +ctx:setFont('16px Arial') -- Font family ignored +ctx:setFont('14px monospace') -- Uses default font at 14px +``` + +**Color Support**: Canvas color parsing is identical to CSS styling - supports hex colors (`#ff0000`), RGB/RGBA (`rgba(255,0,0,0.5)`), Tailwind color names (`red-500`, `slate-800`), and basic named colors (`red`, `blue`, `transparent`). + +##### Using Styles in Drawing + +```lua +-- Set up styles first +ctx:setFillStyle('#ff6b6b') +ctx:setStrokeStyle('#4ecdc4') +ctx:setLineWidth(3) + +-- Then draw with those styles +ctx:fillRect(50, 50, 100, 100) -- Uses fill style +ctx:strokeRect(200, 50, 100, 100) -- Uses stroke style and line width + +-- Styles persist until changed +ctx:setFillStyle('#45b7d1') +ctx:fillRect(50, 200, 100, 100) -- Now uses blue fill +``` + +#### Transformations + +Canvas transformations allow you to modify the coordinate system for drawing operations. + +##### Basic Transformations + +```lua +ctx:save() +ctx:translate(100, 50) +ctx:rotate(math.pi / 4) +ctx:scale(2.0, 1.5) +ctx:fillRect(0, 0, 50, 50) +ctx:restore() +ctx:fillRect(0, 0, 50, 50) +``` + +##### Transformation Examples + +```lua +ctx:save() +ctx:translate(200, 200) +ctx:rotate(math.pi / 6) +ctx:drawText(-25, 0, 'Rotated', 'Arial', '#000000') +ctx:restore() + +for i = 1, 5 do + ctx:save() + ctx:scale(i * 0.3, i * 0.3) + ctx:strokeRect(100, 100, 50, 50) + ctx:restore() +end + +for angle = 0, 360, 30 do + ctx:save() + ctx:translate(200, 200) + ctx:rotate(math.rad(angle)) + ctx:fillRect(50, -5, 40, 10) + ctx:restore() +end +``` + +### Shader Context + +For advanced visual effects, use the shader context: + +```lua +local canvas = gurt.select('#shader-canvas') +local shaderCtx = canvas:withContext('shader') + +shaderCtx:source([[ + shader_type canvas_item; + + uniform float time : hint_range(0.0, 10.0) = 1.0; + uniform vec2 resolution; + + void fragment() { + vec2 uv = UV; + + // Create animated rainbow effect + vec3 color = vec3( + 0.5 + 0.5 * cos(time + uv.x * 6.0), + 0.5 + 0.5 * cos(time + uv.y * 6.0 + 2.0), + 0.5 + 0.5 * cos(time + (uv.x + uv.y) * 6.0 + 4.0) + ); + + COLOR = vec4(color, 1.0); + } +]]) +``` + +## Network API + +### fetch(url, options) + +Makes HTTP requests with full control over method, headers, and body. + +```lua +-- Simple GET request +local response = fetch('https://api.example.com/data') + +-- POST request with data +local response = fetch('https://api.example.com/users', { + method = 'POST', + headers = { + ['Content-Type'] = 'application/json', + ['Authorization'] = 'Bearer token123' + }, + body = JSON.stringify({ + name = 'John Doe', + email = 'john@example.com' + }) +}) + +-- Check response +if response:ok() then + local data = response:json() -- Parse JSON response + local text = response:text() -- Get as text + + trace.log('Status: ' .. response.status) + trace.log('Status Text: ' .. response.statusText) + + -- Access headers + local contentType = response.headers['content-type'] +else + trace.log('Request failed with status: ' .. response.status) +end +``` + +**Supported Methods:** `GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `OPTIONS`, `PATCH` + +**Relative URLs** are automatically resolved to the current domain with `gurt://` protocol. + +## JSON API + +### JSON.stringify(data) + +Converts Lua data to JSON string. + +```lua +local data = { + name = 'Alice', + age = 30, + hobbies = {'reading', 'coding'}, + active = true +} + +local jsonString = JSON.stringify(data) +trace.log(jsonString) -- {"name":"Alice","age":30,"hobbies":["reading","coding"],"active":true} +``` + +### JSON.parse(jsonString) + +Parses JSON string to Lua data. + +```lua +local jsonString = '{"name":"Bob","score":95.5}' +local data, error = JSON.parse(jsonString) + +if data then + trace.log('Name: ' .. data.name) + trace.log('Score: ' .. data.score) +else + trace.log('Parse error: ' .. error) +end +``` + +## Time API + +### Time.now() + +Gets current Unix timestamp. + +```lua +local timestamp = Time.now() +trace.log('Current time: ' .. timestamp) +``` + +### Time.format(timestamp, format) + +Formats a timestamp using format strings. + +```lua +local now = Time.now() +local formatted = Time.format(now, '%Y-%m-%d %H:%M:%S') +trace.log('Formatted: ' .. formatted) + +-- Format strings +-- %Y - Full year (2024) +-- %y - Two-digit year (24) +-- %m - Month (01-12) +-- %d - Day (01-31) +-- %H - Hour 24-format (00-23) +-- %I - Hour 12-format (01-12) +-- %M - Minute (00-59) +-- %S - Second (00-59) +-- %p - AM/PM +-- %A - Full weekday name +-- %a - Abbreviated weekday name +-- %B - Full month name +-- %b - Abbreviated month name +``` + +### Time.date(timestamp) + +Gets date components as a table. + +```lua +local now = Time.now() +local date = Time.date(now) + +trace.log('Year: ' .. date.year) +trace.log('Month: ' .. date.month) +trace.log('Day: ' .. date.day) +trace.log('Hour: ' .. date.hour) +trace.log('Minute: ' .. date.minute) +trace.log('Second: ' .. date.second) +trace.log('Weekday: ' .. date.weekday) -- 0=Sunday, 6=Saturday +``` + +### Time.sleep(seconds) + +Pauses execution for a specified duration. + +```lua +trace.log('Starting...') +Time.sleep(2.0) -- Wait 2 seconds +trace.log('Done waiting!') +``` + +:::note +This blocks the entire Lua thread. Use with caution, we recommend using `setTimeout()` for non-blocking delays. +::: + +### Time.benchmark(function) + +Measures function execution time. + +```lua +local elapsed, result = Time.benchmark(function() + -- Some complex calculation + local sum = 0 + for i = 1, 1000000 do + sum = sum + i + end + return sum +end) + +trace.log('Function took ' .. elapsed .. ' seconds') +trace.log('Result: ' .. result) +``` + +### Time.timer() + +Creates a timer object for measuring intervals. + +```lua +local timer = Time.timer() + +-- Do some work... +Time.sleep(1.5) + +local elapsed = timer:elapsed() +trace.log('Elapsed: ' .. elapsed .. ' seconds') + +timer:reset() -- Reset timer +``` + +### Time.delay(seconds) + +Creates a delay object for non-blocking waits. + +```lua +local delay = Time.delay(3.0) + +-- Check if delay is complete +if delay:complete() then + trace.log('Delay finished!') +end + +-- Get remaining time +local remaining = delay:remaining() +trace.log('Time left: ' .. remaining .. ' seconds') +``` + +## Timeout and Interval Functions + +### setTimeout(callback, milliseconds) + +Executes a function after a delay. + +```lua +local timeoutId = setTimeout(function() + trace.log('This runs after 2 seconds') +end, 2000) + +-- Cancel the timeout +-- clearTimeout(timeoutId) +``` + +### setInterval(callback, milliseconds) + +Executes a function repeatedly at intervals. + +```lua +local intervalId = setInterval(function() + trace.log('This runs every second') +end, 1000) + +setTimeout(function() + clearInterval(intervalId) + trace.log('Interval stopped') +end, 5000) +``` + +### clearTimeout(timeoutId) / clearInterval(intervalId) + +Cancels scheduled timeouts or intervals. + +```lua +local id = setTimeout(function() + trace.log('This will not run') +end, 1000) + +clearTimeout(id) +``` + +## WebSocket API + +Real-time communication with WebSocket servers. + +```lua +local ws = WebSocket.new('ws://localhost:8080/chat') + +ws:on('open', function() + trace.log('WebSocket connected') + ws:send('Hello server!') +end) + +ws:on('message', function(data) + trace.log('Received: ' .. data) +end) + +ws:on('close', function(code, reason) + trace.log('WebSocket closed: ' .. code .. ' - ' .. reason) +end) + +ws:on('error', function(error) + trace.log('WebSocket error: ' .. error) +end) + +ws:send('Hello from client!') +ws:send(JSON.stringify({ type = 'chat', message = 'Hello!' })) + +ws:close() + +if ws.readyState == WebSocket.OPEN then + ws:send('Connected message') +end +``` + +**WebSocket States:** +- `WebSocket.CONNECTING` (0) - Connection in progress +- `WebSocket.OPEN` (1) - Connection established +- `WebSocket.CLOSING` (2) - Connection closing +- `WebSocket.CLOSED` (3) - Connection closed + +## URL API + +URL encoding and decoding utilities for handling special characters in URLs. + +### urlEncode(string) + +Encodes a string for safe use in URLs by converting special characters to percent-encoded format. + +```lua +local encoded = urlEncode('hello world!') +trace.log(encoded) -- hello%20world%21 + +local params = urlEncode('name=John Doe&age=30') +trace.log(params) -- name%3DJohn%20Doe%26age%3D30 + +-- Building query strings +local searchTerm = 'cats & dogs' +local url = 'gurt://search.com/api?q=' .. urlEncode(searchTerm) +trace.log(url) -- gurt://search.com/api?q=cats%20%26%20dogs +``` + +### urlDecode(string) + +Decodes a percent-encoded URL string back to its original form. + +```lua +local decoded = urlDecode('hello%20world%21') +trace.log(decoded) -- hello world! + +local params = urlDecode('name%3DJohn%20Doe%26age%3D30') +trace.log(params) -- name=John Doe&age=30 + +local queryParam = 'cats%20%26%20dogs' +local searchTerm = urlDecode(queryParam) +trace.log(searchTerm) -- cats & dogs +``` + +## Clipboard API + +Write to the system clipboard. + +```lua +Clipboard.write('Hello clipboard!') +``` + +## Regex API + +Pattern matching and text processing with regular expressions. + +### Regex.new(pattern) + +Creates a new regex object from a pattern string. + +```lua +local emailPattern = Regex.new('[a-zA-Z]+@[a-zA-Z]+\\.[a-zA-Z]+') +local phonePattern = Regex.new('\\(\\d{3}\\)\\s*\\d{3}-\\d{4}') +``` + +### regex:test(text) + +Tests if the pattern matches anywhere in the text. Returns `true` or `false`. + +```lua +local pattern = Regex.new('[a-zA-Z]+@[a-zA-Z]+\\.[a-zA-Z]+') + +if pattern:test('user@example.com') then + trace.log('Valid email format') +end + +if pattern:test('Contact us at admin@site.com') then + trace.log('Found email in text') +end +``` + +### regex:match(text) + +Finds the first match and returns capture groups as an array, or `nil` if no match found. + +```lua +local pattern = Regex.new('(\\w+)@(\\w+)\\.(\\w+)') +local result = pattern:match('Contact: admin@site.com for help') + +if result then + trace.log('Full match: ' .. result[1]) -- admin@site.com + trace.log('Username: ' .. result[2]) -- admin + trace.log('Domain: ' .. result[3]) -- site + trace.log('TLD: ' .. result[4]) -- com +else + trace.log('No match found') +end +``` + +## Event Handling + +### Body Events + +Global events that can be captured on the document body: + +```lua +-- Keyboard events +gurt.body:on('keydown', function(event) + trace.log('Key down: ' .. event.key) + if event.ctrl and event.key == 's' then + trace.log('Ctrl+S pressed - Save shortcut!') + end +end) + +gurt.body:on('keyup', function(event) + trace.log('Key up: ' .. event.key) +end) + +gurt.body:on('keypress', function(event) + trace.log('Key pressed: ' .. event.key) + -- Event properties: key, keycode, ctrl, shift, alt +end) + +-- Mouse events +gurt.body:on('mousemove', function(event) + trace.log('Mouse at: ' .. event.x .. ', ' .. event.y) + -- Event properties: x, y, deltaX, deltaY +end) + +gurt.body:on('mouseenter', function() + trace.log('Mouse entered page') +end) + +gurt.body:on('mouseexit', function() + trace.log('Mouse left page') +end) +``` + +### Element Events + +Events specific to DOM elements: + +```lua +local button = gurt.select('#my-button') + +-- Mouse events +button:on('click', function() + trace.log('Button clicked!') +end) + +button:on('mousedown', function() + trace.log('Mouse button pressed') +end) + +button:on('mouseup', function() + trace.log('Mouse button released') +end) + +button:on('mouseenter', function() + trace.log('Mouse entered button') +end) + +button:on('mouseexit', function() + trace.log('Mouse left button') +end) + +button:on('mousemove', function(event) + trace.log('Mouse moved over button: ' .. event.x .. ', ' .. event.y) +end) + +-- Focus events +local input = gurt.select('#text-input') +input:on('focusin', function() + trace.log('Input gained focus') +end) + +input:on('focusout', function() + trace.log('Input lost focus') +end) + +-- Form events +input:on('change', function(event) + trace.log('Input value changed to: ' .. event.value) +end) + +input:on('input', function(event) + trace.log('Input text: ' .. event.value) +end) + +-- For file inputs +local fileInput = gurt.select('#file-input') +fileInput:on('change', function(event) + trace.log('File selected: ' .. event.fileName) +end) + +-- For form submission +local form = gurt.select('#my-form') +form:on('submit', function(event) + trace.log('Form submitted with data:') + for key, value in pairs(event.data) do + trace.log(key .. ': ' .. tostring(value)) + end +end) +``` + +## Error Handling + +### pcall for Protected Calls + +Use Lua's `pcall` for error handling: + +```lua +local success, result = pcall(function() + local data = JSON.parse('invalid json') + return data +end) + +if success then + trace.log('Parse successful: ' .. tostring(result)) +else + trace.log('Parse failed: ' .. result) -- result contains error message +end +``` + +## Additional utilities + +Gurted includes several helpful utilities: + +### print(...) +We modify the global `print()` function to log to the browser console, and also convert any type (e.g. tables) to a readable string. + +```lua +print('Hello, world!') +print({ name = 'Alice', age = 30, hobbies = {'reading', 'coding'} }) -- {age=30,hobbies={1="reading",2="coding"},name="Alice"} +``` + +### table.tostring(table) + +Converts a table to a readable string representation. + +```lua +local data = { name = 'John', age = 30, hobbies = {'reading', 'coding'} } +local str = table.tostring(data) -- {age=30,hobbies={1="reading",2="coding"},name="John"} +``` + +### string.replace(text, search, replacement) + +Replaces the first occurrence of a string or regex pattern. + +```lua +local text = 'Hello world, hello universe' +local result = string.replace(text, 'hello', 'hi') +trace.log(result) -- Hello world, hi universe + +local pattern = Regex.new('\\b\\w+@\\w+\\.\\w+\\b') +local masked = string.replace('Email: john@test.com', pattern, '[EMAIL]') +trace.log(masked) -- Email: [EMAIL] +``` + +### string.replaceAll(text, search, replacement) + +Replaces all occurrences of a string or regex pattern. + +```lua +local text = 'Hello world, hello universe' +local result = string.replaceAll(text, 'hello', 'hi') +trace.log(result) -- Hello world, hi universe + +local pattern = Regex.new('\\b\\w+@\\w+\\.\\w+\\b') +local text = 'Emails: john@test.com, jane@demo.org' +local masked = string.replaceAll(text, pattern, '[EMAIL]') +trace.log(masked) -- Emails: [EMAIL], [EMAIL] +``` + +### string.trim(text) + +Removes whitespace from the beginning and end of a string. + +```lua +local messy = ' Hello World ' +local clean = string.trim(messy) +trace.log('"' .. clean .. '"') -- "Hello World" +``` + +This is particularly useful for debugging and logging complex data structures. diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 8ccef5d..c840a73 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -39,6 +39,7 @@ const sidebars: SidebarsConfig = { items: [ 'html', 'css', + 'lua', 'postprocess', ], }, diff --git a/docs/static/img/docs/tween.png b/docs/static/img/docs/tween.png new file mode 100644 index 0000000..517a02f Binary files /dev/null and b/docs/static/img/docs/tween.png differ diff --git a/flumi/Scenes/Tags/canvas.tscn b/flumi/Scenes/Tags/canvas.tscn new file mode 100644 index 0000000..73a06b7 --- /dev/null +++ b/flumi/Scenes/Tags/canvas.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3 uid="uid://b8x4k5man2m1p"] + +[ext_resource type="Script" uid="uid://08xpof853sfh" path="res://Scripts/Tags/canvas.gd" id="1_canvas"] + +[node name="canvas" type="ColorRect"] +custom_minimum_size = Vector2(300, 150) +offset_right = 300.0 +offset_bottom = 150.0 +size_flags_horizontal = 0 +size_flags_vertical = 0 +color = Color(0, 0, 0, 0) +script = ExtResource("1_canvas") diff --git a/flumi/Scenes/main.tscn b/flumi/Scenes/main.tscn index 57e0c98..a61661d 100644 --- a/flumi/Scenes/main.tscn +++ b/flumi/Scenes/main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=35 format=3 uid="uid://bytm7bt2s4ak8"] +[gd_scene load_steps=36 format=3 uid="uid://bytm7bt2s4ak8"] [ext_resource type="Script" uid="uid://bg5iqnwic1rio" path="res://Scripts/main.gd" id="1_8q3xr"] [ext_resource type="Texture2D" uid="uid://df1m4j7uxi63v" path="res://Assets/Icons/chevron-down.svg" id="2_6bp64"] @@ -9,6 +9,7 @@ [ext_resource type="PackedScene" uid="uid://sqhcxhcre081" path="res://Scenes/Tab.tscn" id="4_344ge"] [ext_resource type="Texture2D" uid="uid://cu4hjoba6etf" path="res://Assets/Icons/rotate-cw.svg" id="5_344ge"] [ext_resource type="Texture2D" uid="uid://cehbtwq6gq0cn" path="res://Assets/Icons/plus.svg" id="5_ynf5e"] +[ext_resource type="Script" uid="uid://nve723radqih" path="res://Scripts/SearchBar.gd" id="9_gt3je"] [ext_resource type="Texture2D" uid="uid://cklatjc4m38dy" path="res://Assets/Icons/ellipsis-vertical.svg" id="10_6iyac"] [ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="11_ee4r6"] [ext_resource type="Script" uid="uid://vjjhljlftlbk" path="res://Scripts/OptionButton.gd" id="11_gt3je"] @@ -201,6 +202,7 @@ size_flags_horizontal = 3 theme = SubResource("Theme_jjvhh") placeholder_text = "Search or enter web address" caret_blink = true +script = ExtResource("9_gt3je") [node name="TextureRect" type="TextureRect" parent="VBoxContainer/HBoxContainer/LineEdit"] layout_mode = 2 diff --git a/flumi/Scripts/B9/Lua.gd b/flumi/Scripts/B9/Lua.gd index 90f80cc..affbfab 100644 --- a/flumi/Scripts/B9/Lua.gd +++ b/flumi/Scripts/B9/Lua.gd @@ -691,6 +691,56 @@ func _handle_dom_operation(operation: Dictionary): _handle_element_focus(operation) "unfocus_element": _handle_element_unfocus(operation) + "canvas_fillRect": + LuaCanvasUtils.handle_canvas_fillRect(operation, dom_parser) + "canvas_strokeRect": + LuaCanvasUtils.handle_canvas_strokeRect(operation, dom_parser) + "canvas_clearRect": + LuaCanvasUtils.handle_canvas_clearRect(operation, dom_parser) + "canvas_drawCircle": + LuaCanvasUtils.handle_canvas_drawCircle(operation, dom_parser) + "canvas_drawText": + LuaCanvasUtils.handle_canvas_drawText(operation, dom_parser) + "canvas_source": + LuaCanvasUtils.handle_canvas_source(operation, dom_parser) + "canvas_beginPath": + LuaCanvasUtils.handle_canvas_beginPath(operation, dom_parser) + "canvas_closePath": + LuaCanvasUtils.handle_canvas_closePath(operation, dom_parser) + "canvas_moveTo": + LuaCanvasUtils.handle_canvas_moveTo(operation, dom_parser) + "canvas_lineTo": + LuaCanvasUtils.handle_canvas_lineTo(operation, dom_parser) + "canvas_arc": + LuaCanvasUtils.handle_canvas_arc(operation, dom_parser) + "canvas_stroke": + LuaCanvasUtils.handle_canvas_stroke(operation, dom_parser) + "canvas_fill": + LuaCanvasUtils.handle_canvas_fill(operation, dom_parser) + # Transformation operations + "canvas_save": + LuaCanvasUtils.handle_canvas_save(operation, dom_parser) + "canvas_restore": + LuaCanvasUtils.handle_canvas_restore(operation, dom_parser) + "canvas_translate": + LuaCanvasUtils.handle_canvas_translate(operation, dom_parser) + "canvas_rotate": + LuaCanvasUtils.handle_canvas_rotate(operation, dom_parser) + "canvas_scale": + LuaCanvasUtils.handle_canvas_scale(operation, dom_parser) + "canvas_quadraticCurveTo": + LuaCanvasUtils.handle_canvas_quadraticCurveTo(operation, dom_parser) + "canvas_bezierCurveTo": + LuaCanvasUtils.handle_canvas_bezierCurveTo(operation, dom_parser) + # Style property operations + "canvas_setStrokeStyle": + LuaCanvasUtils.handle_canvas_setStrokeStyle(operation, dom_parser) + "canvas_setFillStyle": + LuaCanvasUtils.handle_canvas_setFillStyle(operation, dom_parser) + "canvas_setLineWidth": + LuaCanvasUtils.handle_canvas_setLineWidth(operation, dom_parser) + "canvas_setFont": + LuaCanvasUtils.handle_canvas_setFont(operation, dom_parser) _: pass # Unknown operation type, ignore diff --git a/flumi/Scripts/SearchBar.gd b/flumi/Scripts/SearchBar.gd new file mode 100644 index 0000000..9681cec --- /dev/null +++ b/flumi/Scripts/SearchBar.gd @@ -0,0 +1,11 @@ +extends LineEdit + +# NOTE: this should be implemented to every Control element, +# so that it defocuses on click outside element, instead of focusing on another control node, +# but I find it impractical to simply paste to every script in Tags. +# Will hold onto the above for now, and only implement it in SearchBar for now +func _input(event: InputEvent): + if event is InputEventMouseButton and event.is_pressed() and event.button_index == 1: + var evLocal = make_input_local(event) + if !Rect2(Vector2(0,0), size).has_point(evLocal.position): + release_focus() diff --git a/flumi/Scripts/SearchBar.gd.uid b/flumi/Scripts/SearchBar.gd.uid new file mode 100644 index 0000000..85d269f --- /dev/null +++ b/flumi/Scripts/SearchBar.gd.uid @@ -0,0 +1 @@ +uid://nve723radqih diff --git a/flumi/Scripts/Tags/canvas.gd b/flumi/Scripts/Tags/canvas.gd new file mode 100644 index 0000000..93322b5 --- /dev/null +++ b/flumi/Scripts/Tags/canvas.gd @@ -0,0 +1,412 @@ +class_name HTMLCanvas +extends ColorRect + +var canvas_element: HTMLParser.HTMLElement +var parser: HTMLParser +var canvas_width: int = 300 +var canvas_height: int = 150 +var draw_commands: Array = [] +var context_2d: CanvasContext2D = null +var context_shader: CanvasContextShader = null + +class CanvasContext2D: + var canvas: HTMLCanvas + # Drawing state + var current_path: PackedVector2Array = PackedVector2Array() + var path_started: bool = false + # Style properties + var stroke_style: String = "#000000" + var fill_style: String = "#000000" + var line_width: float = 1.0 + var line_cap: String = "butt" + var line_join: String = "miter" + var font: String = "16px sans-serif" + var text_align: String = "start" + var text_baseline: String = "alphabetic" + # Transformation state + var transform_stack: Array = [] + var current_transform: Transform2D = Transform2D.IDENTITY + + func _init(canvas_ref: HTMLCanvas): + canvas = canvas_ref + + func fillRect(x: float, y: float, width: float, height: float, color_hex: String = ""): + var color = _parse_color(fill_style if color_hex.is_empty() else color_hex) + var cmd = { + "type": "fillRect", + "x": x, + "y": y, + "width": width, + "height": height, + "color": color, + "transform": current_transform + } + canvas.draw_commands.append(cmd) + canvas.queue_redraw() + + func strokeRect(x: float, y: float, width: float, height: float, color_hex: String = "", stroke_width: float = 0.0): + var color = _parse_color(stroke_style if color_hex.is_empty() else color_hex) + var width_val = line_width if stroke_width == 0.0 else stroke_width + var cmd = { + "type": "strokeRect", + "x": x, + "y": y, + "width": width, + "height": height, + "color": color, + "stroke_width": width_val, + "transform": current_transform + } + canvas.draw_commands.append(cmd) + canvas.queue_redraw() + + func clearRect(x: float, y: float, width: float, height: float): + var cmd = { + "type": "clearRect", + "x": x, + "y": y, + "width": width, + "height": height + } + canvas.draw_commands.append(cmd) + canvas.queue_redraw() + + func drawCircle(x: float, y: float, radius: float, color_hex: String = "#000000", filled: bool = true): + var cmd = { + "type": "circle", + "x": x, + "y": y, + "radius": radius, + "color": _parse_color(color_hex), + "filled": filled, + "transform": current_transform + } + canvas.draw_commands.append(cmd) + canvas.queue_redraw() + + func drawText(x: float, y: float, text: String, color_hex: String = "#000000"): + var color = _parse_color(fill_style if color_hex == "#000000" else color_hex) + var cmd = { + "type": "text", + "x": x, + "y": y, + "text": text, + "color": color, + "font_size": _parse_font_size(font), + "transform": current_transform + } + canvas.draw_commands.append(cmd) + canvas.queue_redraw() + + # Path-based drawing functions + func beginPath(): + current_path.clear() + path_started = true + + func closePath(): + if current_path.size() > 0: + current_path.append(current_path[0]) + + func moveTo(x: float, y: float): + var point = current_transform * Vector2(x, y) + current_path.clear() + current_path.append(point) + path_started = true + + func lineTo(x: float, y: float): + if not path_started: + moveTo(x, y) + return + var point = current_transform * Vector2(x, y) + current_path.append(point) + + func arc(x: float, y: float, radius: float, start_angle: float, end_angle: float, counterclockwise: bool = false): + var segments = max(8, int(abs(end_angle - start_angle) * radius / 4)) + var angle_step = (end_angle - start_angle) / segments + if counterclockwise: + angle_step = -angle_step + + for i in range(segments + 1): + var angle = start_angle + i * angle_step + var point_x = x + cos(angle) * radius + var point_y = y + sin(angle) * radius + var point = current_transform * Vector2(point_x, point_y) + + if i == 0 and current_path.is_empty(): + current_path.append(point) + else: + current_path.append(point) + + func stroke(): + if current_path.size() < 2: + return + + var cmd = { + "type": "stroke_path", + "path": current_path.duplicate(), + "color": _parse_color(stroke_style), + "line_width": line_width, + "line_cap": line_cap, + "line_join": line_join + } + canvas.draw_commands.append(cmd) + canvas.queue_redraw() + + func fill(): + if current_path.size() < 3: + return + + var cmd = { + "type": "fill_path", + "path": current_path.duplicate(), + "color": _parse_color(fill_style) + } + canvas.draw_commands.append(cmd) + canvas.queue_redraw() + + # Transformation functions + func save(): + transform_stack.append({ + "transform": current_transform, + "stroke_style": stroke_style, + "fill_style": fill_style, + "line_width": line_width, + "line_cap": line_cap, + "line_join": line_join, + "font": font, + "text_align": text_align, + "text_baseline": text_baseline + }) + + func restore(): + if transform_stack.size() > 0: + var state = transform_stack.pop_back() + current_transform = state.transform + stroke_style = state.stroke_style + fill_style = state.fill_style + line_width = state.line_width + line_cap = state.line_cap + line_join = state.line_join + font = state.font + text_align = state.text_align + text_baseline = state.text_baseline + + func translate(x: float, y: float): + current_transform = current_transform.translated(Vector2(x, y)) + + func rotate(angle: float): + var cos_a = cos(angle) + var sin_a = sin(angle) + var new_x = Vector2( + current_transform.x.x * cos_a - current_transform.x.y * sin_a, + current_transform.x.x * sin_a + current_transform.x.y * cos_a + ) + var new_y = Vector2( + current_transform.y.x * cos_a - current_transform.y.y * sin_a, + current_transform.y.x * sin_a + current_transform.y.y * cos_a + ) + current_transform = Transform2D(new_x, new_y, current_transform.origin) + + func scale(x: float, y: float): + current_transform = Transform2D( + current_transform.x * x, + current_transform.y * y, + current_transform.origin + ) + + # Advanced drawing functions + func quadraticCurveTo(cpx: float, cpy: float, x: float, y: float): + if current_path.is_empty(): + moveTo(0, 0) + + var start_point = current_path[current_path.size() - 1] + var control_point = current_transform * Vector2(cpx, cpy) + var end_point = current_transform * Vector2(x, y) + + # Approximate quadratic curve with line segments + var segments = 20 + for i in range(1, segments + 1): + var t = float(i) / segments + var point = start_point.lerp(control_point, t).lerp(control_point.lerp(end_point, t), t) + current_path.append(point) + + func bezierCurveTo(cp1x: float, cp1y: float, cp2x: float, cp2y: float, x: float, y: float): + if current_path.is_empty(): + moveTo(0, 0) + + var start_point = current_path[current_path.size() - 1] + var cp1 = current_transform * Vector2(cp1x, cp1y) + var cp2 = current_transform * Vector2(cp2x, cp2y) + var end_point = current_transform * Vector2(x, y) + + # Approximate cubic bezier with line segments + var segments = 20 + for i in range(1, segments + 1): + var t = float(i) / segments + var inv_t = 1.0 - t + var point = start_point * (inv_t * inv_t * inv_t) + \ + cp1 * (3 * inv_t * inv_t * t) + \ + cp2 * (3 * inv_t * t * t) + \ + end_point * (t * t * t) + current_path.append(point) + + # Style setters + func setFont(font_str: String): + font = font_str + + func setStrokeStyle(style: String): + stroke_style = style + + func setFillStyle(style: String): + fill_style = style + + func setLineWidth(width: float): + line_width = width + + func measureText(text: String) -> Dictionary: + var font_resource = ThemeDB.fallback_font + var font_size = _parse_font_size(font) + var text_size = font_resource.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size) + return {"width": text_size.x} + + func _parse_font_size(font_str: String) -> int: + var regex = RegEx.new() + regex.compile(r"(\d+)px") + var result = regex.search(font_str) + if result: + return int(result.get_string(1)) + return 16 + + func _parse_color(color_str: String) -> Color: + return ColorUtils.parse_color(color_str) + +class CanvasContextShader: + var canvas: HTMLCanvas + + func _init(canvas_ref: HTMLCanvas): + canvas = canvas_ref + + func source(shader_code: String): + var shader = Shader.new() + shader.code = shader_code + + var material = ShaderMaterial.new() + material.shader = shader + + canvas.material = material + +func init(element: HTMLParser.HTMLElement, _parser: HTMLParser): + canvas_element = element + parser = _parser + + var width_attr = element.get_attribute("width", "300") + var height_attr = element.get_attribute("height", "150") + + canvas_width = int(width_attr) + canvas_height = int(height_attr) + + custom_minimum_size = Vector2(canvas_width, canvas_height) + size = Vector2(canvas_width, canvas_height) + color = Color.TRANSPARENT + clip_contents = true + + parser.register_dom_node(element, self) + +func withContext(context_type: String): + match context_type: + "2d": + if context_2d == null: + context_2d = CanvasContext2D.new(self) + return context_2d + "shader": + if context_shader == null: + context_shader = CanvasContextShader.new(self) + return context_shader + _: + return null + +func _draw(): + draw_rect(Rect2(Vector2.ZERO, size), Color.TRANSPARENT) + + for cmd in draw_commands: + match cmd.type: + "fillRect": + var transform = cmd.get("transform", Transform2D.IDENTITY) + + if transform != Transform2D.IDENTITY: + var corners = PackedVector2Array([ + transform * Vector2(cmd.x, cmd.y), + transform * Vector2(cmd.x + cmd.width, cmd.y), + transform * Vector2(cmd.x + cmd.width, cmd.y + cmd.height), + transform * Vector2(cmd.x, cmd.y + cmd.height) + ]) + draw_colored_polygon(corners, cmd.color) + else: + var pos = Vector2(cmd.x, cmd.y) + var sz = Vector2(cmd.width, cmd.height) + draw_rect(Rect2(pos, sz), cmd.color) + "strokeRect": + var transform = cmd.get("transform", Transform2D.IDENTITY) + var stroke_width = cmd.get("stroke_width", 1.0) + + if transform != Transform2D.IDENTITY: + # Draw as stroke polygon for transformed rectangles + var corners = PackedVector2Array([ + transform * Vector2(cmd.x, cmd.y), + transform * Vector2(cmd.x + cmd.width, cmd.y), + transform * Vector2(cmd.x + cmd.width, cmd.y + cmd.height), + transform * Vector2(cmd.x, cmd.y + cmd.height), + transform * Vector2(cmd.x, cmd.y) # Close the path + ]) + for i in range(corners.size() - 1): + draw_line(corners[i], corners[i + 1], cmd.color, stroke_width) + else: + var pos = Vector2(cmd.x, cmd.y) + # Draw stroke as four rectangles + draw_rect(Rect2(pos.x, pos.y, cmd.width, stroke_width), cmd.color) + draw_rect(Rect2(pos.x, pos.y + cmd.height - stroke_width, cmd.width, stroke_width), cmd.color) + draw_rect(Rect2(pos.x, pos.y, stroke_width, cmd.height), cmd.color) + draw_rect(Rect2(pos.x + cmd.width - stroke_width, pos.y, stroke_width, cmd.height), cmd.color) + "clearRect": + # Clear a rectangular area by painting with background color + var clear_rect = Rect2(cmd.x, cmd.y, cmd.width, cmd.height) + # Most canvases have a white or light background - paint with that + draw_rect(clear_rect, Color.WHITE) + "circle": + var transform = cmd.get("transform", Transform2D.IDENTITY) + var center = transform * Vector2(cmd.x, cmd.y) + if cmd.filled: + draw_circle(center, cmd.radius, cmd.color) + else: + # For stroke circle, we need to draw a ring - approximated with arc segments + var segments = 32 + var points = PackedVector2Array() + for i in range(segments + 1): + var angle = (i * TAU) / segments + var point = Vector2(cmd.x + cos(angle) * cmd.radius, cmd.y + sin(angle) * cmd.radius) + points.append(transform * point) + if points.size() > 1: + for i in range(points.size() - 1): + draw_line(points[i], points[i + 1], cmd.color, 1.0) + "text": + var font = ThemeDB.fallback_font + var transform = cmd.get("transform", Transform2D.IDENTITY) + var pos = transform * Vector2(cmd.x, cmd.y) + var font_size = cmd.get("font_size", 16) + draw_string(font, pos, cmd.text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, cmd.color) + "stroke_path": + var path = cmd.path + var clr = cmd.color + var line_width = cmd.get("line_width", 1.0) + if path.size() > 1: + for i in range(path.size() - 1): + draw_line(path[i], path[i + 1], clr, line_width) + "fill_path": + var path = cmd.path + var clr = cmd.color + if path.size() > 2: + draw_colored_polygon(path, clr) + +func clear(): + draw_commands.clear() + queue_redraw() diff --git a/flumi/Scripts/Tags/canvas.gd.uid b/flumi/Scripts/Tags/canvas.gd.uid new file mode 100644 index 0000000..c6bed2b --- /dev/null +++ b/flumi/Scripts/Tags/canvas.gd.uid @@ -0,0 +1 @@ +uid://08xpof853sfh diff --git a/flumi/Scripts/Utils/Lua/Canvas.gd b/flumi/Scripts/Utils/Lua/Canvas.gd new file mode 100644 index 0000000..6780790 --- /dev/null +++ b/flumi/Scripts/Utils/Lua/Canvas.gd @@ -0,0 +1,1020 @@ +class_name LuaCanvasUtils +extends RefCounted + +# This file mainly creates operations that are handled by canvas.gd + +static func emit_canvas_operation(lua_api: LuaAPI, operation: Dictionary) -> void: + lua_api.threaded_vm.call_deferred("_emit_dom_operation_request", operation) + +static func _element_withContext_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + vm.lua_pushnil() + return 1 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var context_type: String = vm.luaL_checkstring(2) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + vm.lua_getfield(1, "_tag_name") + var tag_name: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + # Only works on canvas elements + if tag_name != "canvas": + vm.lua_pushnil() + return 1 + + vm.lua_newtable() + vm.lua_pushstring(element_id) + vm.lua_setfield(-2, "_element_id") + vm.lua_pushstring(context_type) + vm.lua_setfield(-2, "_context_type") + + match context_type: + "2d": + add_2d_context_methods(vm, lua_api) + "shader": + add_shader_context_methods(vm, lua_api) + + return 1 + +static func add_2d_context_methods(vm: LuauVM, lua_api: LuaAPI): + vm.set_meta("lua_api", lua_api) + + # Basic drawing functions + vm.lua_pushcallable(_2d_fillRect_wrapper, "context.fillRect") + vm.lua_setfield(-2, "fillRect") + + vm.lua_pushcallable(_2d_strokeRect_wrapper, "context.strokeRect") + vm.lua_setfield(-2, "strokeRect") + + vm.lua_pushcallable(_2d_clearRect_wrapper, "context.clearRect") + vm.lua_setfield(-2, "clearRect") + + vm.lua_pushcallable(_2d_drawCircle_wrapper, "context.drawCircle") + vm.lua_setfield(-2, "drawCircle") + + vm.lua_pushcallable(_2d_drawText_wrapper, "context.drawText") + vm.lua_setfield(-2, "drawText") + + # Path-based drawing functions + vm.lua_pushcallable(_2d_beginPath_wrapper, "context.beginPath") + vm.lua_setfield(-2, "beginPath") + + vm.lua_pushcallable(_2d_closePath_wrapper, "context.closePath") + vm.lua_setfield(-2, "closePath") + + vm.lua_pushcallable(_2d_moveTo_wrapper, "context.moveTo") + vm.lua_setfield(-2, "moveTo") + + vm.lua_pushcallable(_2d_lineTo_wrapper, "context.lineTo") + vm.lua_setfield(-2, "lineTo") + + vm.lua_pushcallable(_2d_arc_wrapper, "context.arc") + vm.lua_setfield(-2, "arc") + + vm.lua_pushcallable(_2d_stroke_wrapper, "context.stroke") + vm.lua_setfield(-2, "stroke") + + vm.lua_pushcallable(_2d_fill_wrapper, "context.fill") + vm.lua_setfield(-2, "fill") + + # Transformation functions + vm.lua_pushcallable(_2d_save_wrapper, "context.save") + vm.lua_setfield(-2, "save") + + vm.lua_pushcallable(_2d_restore_wrapper, "context.restore") + vm.lua_setfield(-2, "restore") + + vm.lua_pushcallable(_2d_translate_wrapper, "context.translate") + vm.lua_setfield(-2, "translate") + + vm.lua_pushcallable(_2d_rotate_wrapper, "context.rotate") + vm.lua_setfield(-2, "rotate") + + vm.lua_pushcallable(_2d_scale_wrapper, "context.scale") + vm.lua_setfield(-2, "scale") + + # Advanced drawing functions + vm.lua_pushcallable(_2d_quadraticCurveTo_wrapper, "context.quadraticCurveTo") + vm.lua_setfield(-2, "quadraticCurveTo") + + vm.lua_pushcallable(_2d_bezierCurveTo_wrapper, "context.bezierCurveTo") + vm.lua_setfield(-2, "bezierCurveTo") + + # Style property setters + vm.lua_pushcallable(_2d_setStrokeStyle_wrapper, "context.setStrokeStyle") + vm.lua_setfield(-2, "setStrokeStyle") + + vm.lua_pushcallable(_2d_setFillStyle_wrapper, "context.setFillStyle") + vm.lua_setfield(-2, "setFillStyle") + + vm.lua_pushcallable(_2d_setLineWidth_wrapper, "context.setLineWidth") + vm.lua_setfield(-2, "setLineWidth") + + vm.lua_pushcallable(_2d_setFont_wrapper, "context.setFont") + vm.lua_setfield(-2, "setFont") + + # Text measurement + vm.lua_pushcallable(_2d_measureText_wrapper, "context.measureText") + vm.lua_setfield(-2, "measureText") + +static func add_shader_context_methods(vm: LuauVM, lua_api: LuaAPI): + vm.set_meta("lua_api", lua_api) + + vm.lua_pushcallable(_shader_source_wrapper, "context.source") + vm.lua_setfield(-2, "source") + +static func _2d_fillRect_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var x: float = vm.luaL_checknumber(2) + var y: float = vm.luaL_checknumber(3) + var width: float = vm.luaL_checknumber(4) + var height: float = vm.luaL_checknumber(5) + var color: String = "" + + if vm.lua_gettop() >= 6: + color = vm.luaL_checkstring(6) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_fillRect", + "element_id": element_id, + "x": x, + "y": y, + "width": width, + "height": height, + "color": color + } + + emit_canvas_operation(lua_api, operation) + + return 0 + +static func _2d_strokeRect_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var x: float = vm.luaL_checknumber(2) + var y: float = vm.luaL_checknumber(3) + var width: float = vm.luaL_checknumber(4) + var height: float = vm.luaL_checknumber(5) + var color: String = "" + var stroke_width: float = 0.0 + + if vm.lua_gettop() >= 6: + color = vm.luaL_checkstring(6) + if vm.lua_gettop() >= 7: + stroke_width = vm.luaL_checknumber(7) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_strokeRect", + "element_id": element_id, + "x": x, + "y": y, + "width": width, + "height": height, + "color": color, + "stroke_width": stroke_width + } + + emit_canvas_operation(lua_api, operation) + + return 0 + +static func _2d_clearRect_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var x: float = vm.luaL_checknumber(2) + var y: float = vm.luaL_checknumber(3) + var width: float = vm.luaL_checknumber(4) + var height: float = vm.luaL_checknumber(5) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_clearRect", + "element_id": element_id, + "x": x, + "y": y, + "width": width, + "height": height + } + + emit_canvas_operation(lua_api, operation) + + return 0 + +static func _2d_drawCircle_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var x: float = vm.luaL_checknumber(2) + var y: float = vm.luaL_checknumber(3) + var radius: float = vm.luaL_checknumber(4) + var color: String = "#000000" + var filled: bool = true + + if vm.lua_gettop() >= 5: + color = vm.luaL_checkstring(5) + if vm.lua_gettop() >= 6: + filled = vm.lua_toboolean(6) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_drawCircle", + "element_id": element_id, + "x": x, + "y": y, + "radius": radius, + "color": color, + "filled": filled + } + + emit_canvas_operation(lua_api, operation) + + return 0 + +static func _2d_drawText_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var x: float = vm.luaL_checknumber(2) + var y: float = vm.luaL_checknumber(3) + var text: String = vm.luaL_checkstring(4) + var font_name: String = "default" + var color: String = "#000000" + + if vm.lua_gettop() >= 5: + font_name = vm.luaL_checkstring(5) + if vm.lua_gettop() >= 6: + color = vm.luaL_checkstring(6) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_drawText", + "element_id": element_id, + "x": x, + "y": y, + "text": text, + "font_name": font_name, + "color": color + } + + emit_canvas_operation(lua_api, operation) + + return 0 + +static func _shader_source_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var shader_code: String = vm.luaL_checkstring(2) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_source", + "element_id": element_id, + "shader_code": shader_code + } + + emit_canvas_operation(lua_api, operation) + + return 0 + +# Path-based drawing wrappers +static func _2d_beginPath_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_beginPath", + "element_id": element_id + } + + emit_canvas_operation(lua_api, operation) + return 0 + +static func _2d_closePath_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_closePath", + "element_id": element_id + } + + emit_canvas_operation(lua_api, operation) + return 0 + +static func _2d_moveTo_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var x: float = vm.luaL_checknumber(2) + var y: float = vm.luaL_checknumber(3) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_moveTo", + "element_id": element_id, + "x": x, + "y": y + } + + emit_canvas_operation(lua_api, operation) + return 0 + +static func _2d_lineTo_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var x: float = vm.luaL_checknumber(2) + var y: float = vm.luaL_checknumber(3) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_lineTo", + "element_id": element_id, + "x": x, + "y": y + } + + emit_canvas_operation(lua_api, operation) + return 0 + +static func _2d_arc_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var x: float = vm.luaL_checknumber(2) + var y: float = vm.luaL_checknumber(3) + var radius: float = vm.luaL_checknumber(4) + var start_angle: float = vm.luaL_checknumber(5) + var end_angle: float = vm.luaL_checknumber(6) + var counterclockwise: bool = false + + if vm.lua_gettop() >= 7: + counterclockwise = vm.lua_toboolean(7) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_arc", + "element_id": element_id, + "x": x, + "y": y, + "radius": radius, + "start_angle": start_angle, + "end_angle": end_angle, + "counterclockwise": counterclockwise + } + + emit_canvas_operation(lua_api, operation) + return 0 + +static func _2d_stroke_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_stroke", + "element_id": element_id + } + + emit_canvas_operation(lua_api, operation) + return 0 + +static func _2d_fill_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_fill", + "element_id": element_id + } + + emit_canvas_operation(lua_api, operation) + return 0 + +# Transformation wrappers +static func _2d_save_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_save", + "element_id": element_id + } + + emit_canvas_operation(lua_api, operation) + return 0 + +static func _2d_restore_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_restore", + "element_id": element_id + } + + emit_canvas_operation(lua_api, operation) + return 0 + +static func _2d_translate_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var x: float = vm.luaL_checknumber(2) + var y: float = vm.luaL_checknumber(3) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_translate", + "element_id": element_id, + "x": x, + "y": y + } + + emit_canvas_operation(lua_api, operation) + return 0 + +static func _2d_rotate_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var angle: float = vm.luaL_checknumber(2) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_rotate", + "element_id": element_id, + "angle": angle + } + + emit_canvas_operation(lua_api, operation) + return 0 + +static func _2d_scale_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var x: float = vm.luaL_checknumber(2) + var y: float = vm.luaL_checknumber(3) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_scale", + "element_id": element_id, + "x": x, + "y": y + } + + emit_canvas_operation(lua_api, operation) + return 0 + +# Advanced drawing wrappers +static func _2d_quadraticCurveTo_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var cpx: float = vm.luaL_checknumber(2) + var cpy: float = vm.luaL_checknumber(3) + var x: float = vm.luaL_checknumber(4) + var y: float = vm.luaL_checknumber(5) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_quadraticCurveTo", + "element_id": element_id, + "cpx": cpx, + "cpy": cpy, + "x": x, + "y": y + } + + emit_canvas_operation(lua_api, operation) + return 0 + +static func _2d_bezierCurveTo_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var cp1x: float = vm.luaL_checknumber(2) + var cp1y: float = vm.luaL_checknumber(3) + var cp2x: float = vm.luaL_checknumber(4) + var cp2y: float = vm.luaL_checknumber(5) + var x: float = vm.luaL_checknumber(6) + var y: float = vm.luaL_checknumber(7) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_bezierCurveTo", + "element_id": element_id, + "cp1x": cp1x, + "cp1y": cp1y, + "cp2x": cp2x, + "cp2y": cp2y, + "x": x, + "y": y + } + + emit_canvas_operation(lua_api, operation) + return 0 + +# Style property wrappers +static func _2d_setStrokeStyle_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var style: String = vm.luaL_checkstring(2) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_setStrokeStyle", + "element_id": element_id, + "style": style + } + + emit_canvas_operation(lua_api, operation) + return 0 + +static func _2d_setFillStyle_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var style: String = vm.luaL_checkstring(2) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_setFillStyle", + "element_id": element_id, + "style": style + } + + emit_canvas_operation(lua_api, operation) + return 0 + +static func _2d_setLineWidth_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var width: float = vm.luaL_checknumber(2) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_setLineWidth", + "element_id": element_id, + "width": width + } + + emit_canvas_operation(lua_api, operation) + return 0 + +static func _2d_setFont_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var font: String = vm.luaL_checkstring(2) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var operation = { + "type": "canvas_setFont", + "element_id": element_id, + "font": font + } + + emit_canvas_operation(lua_api, operation) + return 0 + +# TODO: should probably cache and async this, could lag otherwise if called looped +static func _2d_measureText_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + vm.lua_pushnil() + return 1 + + vm.luaL_checktype(1, vm.LUA_TTABLE) + var text: String = vm.luaL_checkstring(2) + + vm.lua_getfield(1, "_element_id") + var element_id = vm.lua_tostring(-1) + vm.lua_pop(1) + + var dom_parser = lua_api.dom_parser + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + var actual_width = text.length() * 10 # Fallback estimate + + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context and context.has_method("measureText"): + var metrics = context.measureText(text) + if metrics and metrics.has("width"): + actual_width = metrics.width + + vm.lua_newtable() + vm.lua_pushnumber(actual_width) + vm.lua_setfield(-2, "width") + + return 1 + +static func handle_canvas_fillRect(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var x: float = operation.x + var y: float = operation.y + var width: float = operation.width + var height: float = operation.height + var color: String = operation.color + + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.fillRect(x, y, width, height, color) + +static func handle_canvas_strokeRect(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var x: float = operation.x + var y: float = operation.y + var width: float = operation.width + var height: float = operation.height + var color: String = operation.color + var stroke_width: float = operation.get("stroke_width", 1.0) + + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.strokeRect(x, y, width, height, color, stroke_width) + +static func handle_canvas_clearRect(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var x: float = operation.x + var y: float = operation.y + var width: float = operation.width + var height: float = operation.height + + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.clearRect(x, y, width, height) + +static func handle_canvas_drawCircle(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var x: float = operation.x + var y: float = operation.y + var radius: float = operation.radius + var color: String = operation.color + var filled: bool = operation.get("filled", true) + + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.drawCircle(x, y, radius, color, filled) + +static func handle_canvas_drawText(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var x: float = operation.x + var y: float = operation.y + var text: String = operation.text + var color: String = operation.color + + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.drawText(x, y, text, color) + +static func handle_canvas_source(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var shader_code: String = operation.shader_code + + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("shader") + if context: + context.source(shader_code) + +static func handle_canvas_beginPath(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.beginPath() + +static func handle_canvas_closePath(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.closePath() + +static func handle_canvas_moveTo(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var x: float = operation.x + var y: float = operation.y + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.moveTo(x, y) + +static func handle_canvas_lineTo(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var x: float = operation.x + var y: float = operation.y + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.lineTo(x, y) + +static func handle_canvas_arc(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var x: float = operation.x + var y: float = operation.y + var radius: float = operation.radius + var start_angle: float = operation.start_angle + var end_angle: float = operation.end_angle + var counterclockwise: bool = operation.get("counterclockwise", false) + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.arc(x, y, radius, start_angle, end_angle, counterclockwise) + +static func handle_canvas_stroke(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.stroke() + +static func handle_canvas_fill(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.fill() + +static func handle_canvas_save(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.save() + +static func handle_canvas_restore(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.restore() + +static func handle_canvas_translate(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var x: float = operation.x + var y: float = operation.y + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.translate(x, y) + +static func handle_canvas_rotate(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var angle: float = operation.angle + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.rotate(angle) + +static func handle_canvas_scale(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var x: float = operation.x + var y: float = operation.y + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.scale(x, y) + +static func handle_canvas_quadraticCurveTo(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var cpx: float = operation.cpx + var cpy: float = operation.cpy + var x: float = operation.x + var y: float = operation.y + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.quadraticCurveTo(cpx, cpy, x, y) + +static func handle_canvas_bezierCurveTo(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var cp1x: float = operation.cp1x + var cp1y: float = operation.cp1y + var cp2x: float = operation.cp2x + var cp2y: float = operation.cp2y + var x: float = operation.x + var y: float = operation.y + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) + +static func handle_canvas_setStrokeStyle(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var style: String = operation.style + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.stroke_style = style + +static func handle_canvas_setFillStyle(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var style: String = operation.style + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.fill_style = style + +static func handle_canvas_setLineWidth(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var width: float = operation.width + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.line_width = width + +static func handle_canvas_setFont(operation: Dictionary, dom_parser: HTMLParser) -> void: + var element_id: String = operation.element_id + var font: String = operation.font + var dom_node = get_canvas_node_main_thread(dom_parser, element_id) + if dom_node and dom_node.has_method("withContext"): + var context = dom_node.withContext("2d") + if context: + context.font = font + +static func get_canvas_node_main_thread(dom_parser: HTMLParser, element_id: String) -> Node: + var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null) + if not dom_node: + return null + + if dom_node is MarginContainer and dom_node.get_child_count() > 0: + dom_node = dom_node.get_child(0) + + return dom_node diff --git a/flumi/Scripts/Utils/Lua/Canvas.gd.uid b/flumi/Scripts/Utils/Lua/Canvas.gd.uid new file mode 100644 index 0000000..b6f1b7d --- /dev/null +++ b/flumi/Scripts/Utils/Lua/Canvas.gd.uid @@ -0,0 +1 @@ +uid://cyngvqm6wghoh diff --git a/flumi/Scripts/Utils/Lua/DOM.gd b/flumi/Scripts/Utils/Lua/DOM.gd index acb2045..2197d70 100644 --- a/flumi/Scripts/Utils/Lua/DOM.gd +++ b/flumi/Scripts/Utils/Lua/DOM.gd @@ -645,6 +645,9 @@ static func add_element_methods(vm: LuauVM, lua_api: LuaAPI) -> void: vm.lua_pushcallable(LuaDOMUtils._element_unfocus_wrapper, "element.unfocus") vm.lua_setfield(-2, "unfocus") + vm.lua_pushcallable(LuaCanvasUtils._element_withContext_wrapper, "element.withContext") + vm.lua_setfield(-2, "withContext") + add_classlist_support(vm) vm.lua_newtable() diff --git a/flumi/Scripts/main.gd b/flumi/Scripts/main.gd index 6982339..532b2f3 100644 --- a/flumi/Scripts/main.gd +++ b/flumi/Scripts/main.gd @@ -30,6 +30,7 @@ const TEXTAREA = preload("res://Scenes/Tags/textarea.tscn") const DIV = preload("res://Scenes/Tags/div.tscn") const AUDIO = preload("res://Scenes/Tags/audio.tscn") const POSTPROCESS = preload("res://Scenes/Tags/postprocess.tscn") +const CANVAS = preload("res://Scenes/Tags/canvas.tscn") const MIN_SIZE = Vector2i(750, 200) @@ -324,7 +325,7 @@ func render_content(html_bytes: PackedByteArray) -> void: if element_node: # Input elements register their own DOM nodes in their init() function - if element.tag_name not in ["input", "textarea", "select", "button", "audio"]: + if element.tag_name not in ["input", "textarea", "select", "button", "audio", "canvas"]: parser.register_dom_node(element, element_node) # ul/ol handle their own adding @@ -606,6 +607,9 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP "audio": node = AUDIO.instantiate() node.init(element, parser) + "canvas": + node = CANVAS.instantiate() + node.init(element, parser) "div": var styles = parser.get_element_styles_with_inheritance(element, "", []) var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", []) diff --git a/tests/canvas.html b/tests/canvas.html new file mode 100644 index 0000000..f6f906e --- /dev/null +++ b/tests/canvas.html @@ -0,0 +1,363 @@ + + Complete Canvas API Test - GURT Browser + + + + + + + + + + +

🎨 Complete Canvas API Test Suite

+ +
+
+

📋 API Coverage Test

+

This page tests every single Canvas 2D API function implemented in GURT:

+
    +
  • Basic Drawing: fillRect, strokeRect, clearRect, drawCircle, drawText
  • +
  • Path Drawing: beginPath, closePath, moveTo, lineTo, arc, stroke, fill
  • +
  • Transformations: save, restore, translate, rotate, scale
  • +
  • Advanced Paths: quadraticCurveTo, bezierCurveTo
  • +
  • Styling: setStrokeStyle, setFillStyle, setLineWidth, setFont
  • +
  • Text Measurement: measureText
  • +
  • Shader Support: withContext("shader"), source
  • +
+
+ +
+ +
+

Basic Drawing Functions

+

Rectangle and circle primitives

+ +
+ +
+

Path-Based Drawing

+

Paths, lines, arcs, and shapes

+ +
+ + +
+

Transformations

+

Translate, rotate, scale, save/restore

+ +
+ +
+

Curves & Advanced Paths

+

Quadratic and Bezier curves

+ +
+ + +
+

Styles & Text

+

Colors, line widths, fonts, text measurement

+ +
+ +
+

Complex Composition

+

Multiple techniques combined

+ +
+
+ + +
+

Shader Canvas Support

+

Custom fragment shader with animated colors

+
+ +
+
+ +
+

✅ Test Results

+

All canvas functions should render correctly above. Check the browser console for any errors.

+

This test demonstrates compatibility with HTML5 Canvas API standards.

+
+
+ \ No newline at end of file diff --git a/tests/snake.html b/tests/snake.html new file mode 100644 index 0000000..d55e96c --- /dev/null +++ b/tests/snake.html @@ -0,0 +1,237 @@ + + Snake Game - Canvas Demo + + + + + + + + + + +

🐍 Snake Game

+ +
+
+
+
Score: 0
+ +
+
+ +
+ +
+ +
Test
+ +
+

Controls:

+

Use Arrow Keys or WASD to control the snake

+
    +
  • 🔼 Up: Arrow Up or W
  • +
  • 🔽 Down: Arrow Down or S
  • +
  • ◀️ Left: Arrow Left or A
  • +
  • ▶️ Right: Arrow Right or D
  • +
+

Eat the red food to grow and increase your score!

+
+
+ \ No newline at end of file