canvas API
This commit is contained in:
1267
docs/docs/lua.md
Normal file
1267
docs/docs/lua.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -39,6 +39,7 @@ const sidebars: SidebarsConfig = {
|
|||||||
items: [
|
items: [
|
||||||
'html',
|
'html',
|
||||||
'css',
|
'css',
|
||||||
|
'lua',
|
||||||
'postprocess',
|
'postprocess',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
docs/static/img/docs/tween.png
vendored
Normal file
BIN
docs/static/img/docs/tween.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 217 KiB |
12
flumi/Scenes/Tags/canvas.tscn
Normal file
12
flumi/Scenes/Tags/canvas.tscn
Normal file
@@ -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")
|
||||||
@@ -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="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"]
|
[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="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://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="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="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="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"]
|
[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")
|
theme = SubResource("Theme_jjvhh")
|
||||||
placeholder_text = "Search or enter web address"
|
placeholder_text = "Search or enter web address"
|
||||||
caret_blink = true
|
caret_blink = true
|
||||||
|
script = ExtResource("9_gt3je")
|
||||||
|
|
||||||
[node name="TextureRect" type="TextureRect" parent="VBoxContainer/HBoxContainer/LineEdit"]
|
[node name="TextureRect" type="TextureRect" parent="VBoxContainer/HBoxContainer/LineEdit"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|||||||
@@ -691,6 +691,56 @@ func _handle_dom_operation(operation: Dictionary):
|
|||||||
_handle_element_focus(operation)
|
_handle_element_focus(operation)
|
||||||
"unfocus_element":
|
"unfocus_element":
|
||||||
_handle_element_unfocus(operation)
|
_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
|
pass # Unknown operation type, ignore
|
||||||
|
|
||||||
|
|||||||
11
flumi/Scripts/SearchBar.gd
Normal file
11
flumi/Scripts/SearchBar.gd
Normal file
@@ -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()
|
||||||
1
flumi/Scripts/SearchBar.gd.uid
Normal file
1
flumi/Scripts/SearchBar.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://nve723radqih
|
||||||
412
flumi/Scripts/Tags/canvas.gd
Normal file
412
flumi/Scripts/Tags/canvas.gd
Normal file
@@ -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()
|
||||||
1
flumi/Scripts/Tags/canvas.gd.uid
Normal file
1
flumi/Scripts/Tags/canvas.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://08xpof853sfh
|
||||||
1020
flumi/Scripts/Utils/Lua/Canvas.gd
Normal file
1020
flumi/Scripts/Utils/Lua/Canvas.gd
Normal file
File diff suppressed because it is too large
Load Diff
1
flumi/Scripts/Utils/Lua/Canvas.gd.uid
Normal file
1
flumi/Scripts/Utils/Lua/Canvas.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cyngvqm6wghoh
|
||||||
@@ -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_pushcallable(LuaDOMUtils._element_unfocus_wrapper, "element.unfocus")
|
||||||
vm.lua_setfield(-2, "unfocus")
|
vm.lua_setfield(-2, "unfocus")
|
||||||
|
|
||||||
|
vm.lua_pushcallable(LuaCanvasUtils._element_withContext_wrapper, "element.withContext")
|
||||||
|
vm.lua_setfield(-2, "withContext")
|
||||||
|
|
||||||
add_classlist_support(vm)
|
add_classlist_support(vm)
|
||||||
|
|
||||||
vm.lua_newtable()
|
vm.lua_newtable()
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const TEXTAREA = preload("res://Scenes/Tags/textarea.tscn")
|
|||||||
const DIV = preload("res://Scenes/Tags/div.tscn")
|
const DIV = preload("res://Scenes/Tags/div.tscn")
|
||||||
const AUDIO = preload("res://Scenes/Tags/audio.tscn")
|
const AUDIO = preload("res://Scenes/Tags/audio.tscn")
|
||||||
const POSTPROCESS = preload("res://Scenes/Tags/postprocess.tscn")
|
const POSTPROCESS = preload("res://Scenes/Tags/postprocess.tscn")
|
||||||
|
const CANVAS = preload("res://Scenes/Tags/canvas.tscn")
|
||||||
|
|
||||||
const MIN_SIZE = Vector2i(750, 200)
|
const MIN_SIZE = Vector2i(750, 200)
|
||||||
|
|
||||||
@@ -324,7 +325,7 @@ func render_content(html_bytes: PackedByteArray) -> void:
|
|||||||
if element_node:
|
if element_node:
|
||||||
|
|
||||||
# Input elements register their own DOM nodes in their init() function
|
# 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)
|
parser.register_dom_node(element, element_node)
|
||||||
|
|
||||||
# ul/ol handle their own adding
|
# ul/ol handle their own adding
|
||||||
@@ -606,6 +607,9 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
|
|||||||
"audio":
|
"audio":
|
||||||
node = AUDIO.instantiate()
|
node = AUDIO.instantiate()
|
||||||
node.init(element, parser)
|
node.init(element, parser)
|
||||||
|
"canvas":
|
||||||
|
node = CANVAS.instantiate()
|
||||||
|
node.init(element, parser)
|
||||||
"div":
|
"div":
|
||||||
var styles = parser.get_element_styles_with_inheritance(element, "", [])
|
var styles = parser.get_element_styles_with_inheritance(element, "", [])
|
||||||
var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", [])
|
var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", [])
|
||||||
|
|||||||
363
tests/canvas.html
Normal file
363
tests/canvas.html
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
<head>
|
||||||
|
<title>Complete Canvas API Test - GURT Browser</title>
|
||||||
|
<icon src="https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Lua-Logo.svg/256px-Lua-Logo.svg.png">
|
||||||
|
<meta name="theme-color" content="#8b5cf6">
|
||||||
|
<meta name="description" content="Comprehensive test of all Canvas 2D API functions">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body { bg-[#f8fafc] p-6 }
|
||||||
|
h1 { text-[#8b5cf6] text-3xl font-bold text-center }
|
||||||
|
h2 { text-[#7c3aed] text-xl font-semibold mb-3 }
|
||||||
|
h3 { text-[#6d28d9] text-lg font-medium mb-2 }
|
||||||
|
.container { bg-[#ffffff] p-6 rounded-lg shadow-lg max-w-6xl mx-auto }
|
||||||
|
.canvas-grid { display-grid grid-cols-2 gap-6 }
|
||||||
|
.test-section { bg-[#f8fafc] p-4 rounded-lg border border-[#e2e8f0] mb-4 }
|
||||||
|
.canvas-item { text-center p-4 }
|
||||||
|
.description { text-[#64748b] text-sm mb-2 }
|
||||||
|
canvas { border border-[#cbd5e1] rounded-lg bg-white }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
gurt.log('Starting comprehensive Canvas API test...')
|
||||||
|
|
||||||
|
-- Test 1: Basic Rectangle and Circle Drawing
|
||||||
|
local basicCanvas = gurt.select("#basic-canvas")
|
||||||
|
local basicCtx = basicCanvas:withContext("2d")
|
||||||
|
|
||||||
|
-- Background
|
||||||
|
basicCtx:fillRect(0, 0, 400, 300, "#f1f5f9")
|
||||||
|
|
||||||
|
-- Basic rectangles
|
||||||
|
basicCtx:fillRect(20, 20, 60, 40, "#ef4444")
|
||||||
|
basicCtx:strokeRect(100, 20, 60, 40, "#22c55e", 3)
|
||||||
|
basicCtx:clearRect(30, 30, 20, 20)
|
||||||
|
|
||||||
|
-- Circles
|
||||||
|
basicCtx:drawCircle(200, 50, 25, "#3b82f6", true)
|
||||||
|
basicCtx:drawCircle(280, 50, 25, "#f59e0b", false)
|
||||||
|
|
||||||
|
-- Text
|
||||||
|
basicCtx:drawText(20, 100, "Basic Drawing Functions", "#1f2937")
|
||||||
|
basicCtx:drawText(20, 120, "fillRect, strokeRect, clearRect, drawCircle, drawText", "#6b7280")
|
||||||
|
|
||||||
|
-- Test 2: Path-Based Drawing
|
||||||
|
local pathCanvas = gurt.select("#path-canvas")
|
||||||
|
local pathCtx = pathCanvas:withContext("2d")
|
||||||
|
|
||||||
|
pathCtx:fillRect(0, 0, 400, 300, "#f1f5f9")
|
||||||
|
pathCtx:setStrokeStyle("#dc2626")
|
||||||
|
pathCtx:setFillStyle("#fecaca")
|
||||||
|
pathCtx:setLineWidth(3)
|
||||||
|
|
||||||
|
-- Simple test - draw two straight lines first
|
||||||
|
pathCtx:setStrokeStyle("#ff0000")
|
||||||
|
pathCtx:setLineWidth(5)
|
||||||
|
pathCtx:beginPath()
|
||||||
|
pathCtx:moveTo(50, 50)
|
||||||
|
pathCtx:lineTo(150, 50)
|
||||||
|
pathCtx:stroke()
|
||||||
|
|
||||||
|
-- Triangle
|
||||||
|
pathCtx:beginPath()
|
||||||
|
pathCtx:moveTo(50, 100)
|
||||||
|
pathCtx:lineTo(100, 100)
|
||||||
|
pathCtx:lineTo(75, 150)
|
||||||
|
pathCtx:closePath()
|
||||||
|
pathCtx:fill()
|
||||||
|
pathCtx:stroke()
|
||||||
|
|
||||||
|
-- Complex path
|
||||||
|
pathCtx:setStrokeStyle("#2563eb")
|
||||||
|
pathCtx:beginPath()
|
||||||
|
pathCtx:moveTo(150, 50)
|
||||||
|
pathCtx:lineTo(200, 70)
|
||||||
|
pathCtx:lineTo(180, 120)
|
||||||
|
pathCtx:lineTo(150, 100)
|
||||||
|
pathCtx:stroke()
|
||||||
|
|
||||||
|
-- Arc/Circle path
|
||||||
|
pathCtx:setFillStyle("#10b981")
|
||||||
|
pathCtx:beginPath()
|
||||||
|
pathCtx:arc(300, 75, 30, 0, 2 * math.pi, false)
|
||||||
|
pathCtx:fill()
|
||||||
|
|
||||||
|
-- Half circle
|
||||||
|
pathCtx:setStrokeStyle("#f59e0b")
|
||||||
|
pathCtx:setLineWidth(4)
|
||||||
|
pathCtx:beginPath()
|
||||||
|
pathCtx:arc(350, 75, 25, 0, math.pi, false)
|
||||||
|
pathCtx:stroke()
|
||||||
|
|
||||||
|
pathCtx:drawText(20, 200, "Path-Based Drawing", "#1f2937")
|
||||||
|
pathCtx:drawText(20, 220, "beginPath, moveTo, lineTo, closePath, arc, stroke, fill", "#6b7280")
|
||||||
|
|
||||||
|
-- Test 3: Transformations
|
||||||
|
local transformCanvas = gurt.select("#transform-canvas")
|
||||||
|
local transformCtx = transformCanvas:withContext("2d")
|
||||||
|
|
||||||
|
-- Background
|
||||||
|
transformCtx:setFillStyle("#f1f5f9")
|
||||||
|
transformCtx:fillRect(0, 0, 400, 300)
|
||||||
|
|
||||||
|
-- Original square (no transform)
|
||||||
|
transformCtx:setFillStyle("#e2e8f0")
|
||||||
|
transformCtx:fillRect(50, 50, 40, 40)
|
||||||
|
|
||||||
|
-- Simple translation test
|
||||||
|
transformCtx:save()
|
||||||
|
transformCtx:translate(100, 0)
|
||||||
|
transformCtx:setFillStyle("#ef4444")
|
||||||
|
transformCtx:fillRect(50, 50, 40, 40) -- Should appear at (150, 50)
|
||||||
|
transformCtx:restore()
|
||||||
|
|
||||||
|
-- Simple rotation test
|
||||||
|
transformCtx:save()
|
||||||
|
transformCtx:translate(200, 70) -- Move origin to (200,70)
|
||||||
|
transformCtx:rotate(math.pi / 4) -- Rotate 45 degrees
|
||||||
|
transformCtx:setFillStyle("#22c55e")
|
||||||
|
transformCtx:fillRect(-20, -20, 40, 40) -- Draw centered on new origin
|
||||||
|
transformCtx:restore()
|
||||||
|
|
||||||
|
-- Simple scaling test
|
||||||
|
transformCtx:save()
|
||||||
|
transformCtx:translate(300, 70)
|
||||||
|
transformCtx:scale(1.5, 0.8)
|
||||||
|
transformCtx:setFillStyle("#3b82f6")
|
||||||
|
transformCtx:fillRect(-20, -20, 40, 40)
|
||||||
|
transformCtx:restore()
|
||||||
|
|
||||||
|
-- Combined test
|
||||||
|
transformCtx:save()
|
||||||
|
transformCtx:translate(150, 180)
|
||||||
|
transformCtx:rotate(math.pi / 6)
|
||||||
|
transformCtx:scale(1.2, 1.2)
|
||||||
|
transformCtx:setFillStyle("#f59e0b")
|
||||||
|
transformCtx:fillRect(-25, -25, 50, 50)
|
||||||
|
transformCtx:restore()
|
||||||
|
|
||||||
|
transformCtx:drawText(20, 250, "Transformations", "#1f2937")
|
||||||
|
transformCtx:drawText(20, 270, "translate, rotate, scale, save, restore", "#6b7280")
|
||||||
|
|
||||||
|
-- Test 4: Curves and Advanced Paths
|
||||||
|
local curveCanvas = gurt.select("#curve-canvas")
|
||||||
|
local curveCtx = curveCanvas:withContext("2d")
|
||||||
|
|
||||||
|
curveCtx:fillRect(0, 0, 400, 300, "#f1f5f9")
|
||||||
|
curveCtx:setStrokeStyle("#8b5cf6")
|
||||||
|
curveCtx:setLineWidth(3)
|
||||||
|
|
||||||
|
-- Quadratic curve
|
||||||
|
curveCtx:beginPath()
|
||||||
|
curveCtx:moveTo(50, 50)
|
||||||
|
curveCtx:quadraticCurveTo(100, 20, 150, 50)
|
||||||
|
curveCtx:stroke()
|
||||||
|
|
||||||
|
-- Bezier curve
|
||||||
|
curveCtx:setStrokeStyle("#dc2626")
|
||||||
|
curveCtx:beginPath()
|
||||||
|
curveCtx:moveTo(200, 50)
|
||||||
|
curveCtx:bezierCurveTo(220, 20, 280, 20, 300, 50)
|
||||||
|
curveCtx:stroke()
|
||||||
|
|
||||||
|
-- Complex curved shape
|
||||||
|
curveCtx:setStrokeStyle("#10b981")
|
||||||
|
curveCtx:setFillStyle("#bbf7d0")
|
||||||
|
curveCtx:beginPath()
|
||||||
|
curveCtx:moveTo(100, 120)
|
||||||
|
curveCtx:quadraticCurveTo(150, 90, 200, 120)
|
||||||
|
curveCtx:quadraticCurveTo(230, 150, 200, 180)
|
||||||
|
curveCtx:quadraticCurveTo(150, 210, 100, 180)
|
||||||
|
curveCtx:quadraticCurveTo(70, 150, 100, 120)
|
||||||
|
curveCtx:fill()
|
||||||
|
curveCtx:stroke()
|
||||||
|
|
||||||
|
curveCtx:drawText(20, 250, "Curves and Advanced Paths", "#1f2937")
|
||||||
|
curveCtx:drawText(20, 270, "quadraticCurveTo, bezierCurveTo", "#6b7280")
|
||||||
|
|
||||||
|
-- Test 5: Style Properties and Text
|
||||||
|
local styleCanvas = gurt.select("#style-canvas")
|
||||||
|
local styleCtx = styleCanvas:withContext("2d")
|
||||||
|
|
||||||
|
styleCtx:fillRect(0, 0, 400, 300, "#f1f5f9")
|
||||||
|
|
||||||
|
-- Different line widths
|
||||||
|
for i = 1, 5 do
|
||||||
|
styleCtx:setLineWidth(i * 2)
|
||||||
|
styleCtx:setStrokeStyle("#" .. string.format("%02x", i * 40) .. "4040")
|
||||||
|
styleCtx:beginPath()
|
||||||
|
styleCtx:moveTo(20, 20 + i * 15)
|
||||||
|
styleCtx:lineTo(120, 20 + i * 15)
|
||||||
|
styleCtx:stroke()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Different colors
|
||||||
|
local colors = {"#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4", "#3b82f6", "#8b5cf6", "#ec4899"}
|
||||||
|
for i, color in ipairs(colors) do
|
||||||
|
styleCtx:setFillStyle(color)
|
||||||
|
styleCtx:fillRect(140 + (i-1) * 25, 30, 20, 60)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Text with different properties
|
||||||
|
styleCtx:setFont("20px sans-serif")
|
||||||
|
styleCtx:drawText(20, 140, "Font Size Test", "#1f2937")
|
||||||
|
|
||||||
|
styleCtx:setFont("16px sans-serif")
|
||||||
|
styleCtx:drawText(20, 170, "Medium Size Text", "#374151")
|
||||||
|
|
||||||
|
styleCtx:setFont("14px sans-serif")
|
||||||
|
styleCtx:drawText(20, 190, "Small Size Text", "#4b5563")
|
||||||
|
|
||||||
|
-- Text measurement demo
|
||||||
|
local testText = "Measured Text"
|
||||||
|
local metrics = styleCtx:measureText(testText)
|
||||||
|
styleCtx:drawText(200, 170, testText, "#dc2626")
|
||||||
|
styleCtx:setStrokeStyle("#dc2626")
|
||||||
|
styleCtx:strokeRect(200, 155, metrics.width, 20, "#dc2626", 1)
|
||||||
|
|
||||||
|
styleCtx:drawText(20, 250, "Styles and Text", "#1f2937")
|
||||||
|
styleCtx:drawText(20, 270, "setStrokeStyle, setFillStyle, setLineWidth, setFont, measureText", "#6b7280")
|
||||||
|
|
||||||
|
-- Test 6: Complex Composition
|
||||||
|
local complexCanvas = gurt.select("#complex-canvas")
|
||||||
|
local complexCtx = complexCanvas:withContext("2d")
|
||||||
|
|
||||||
|
complexCtx:fillRect(0, 0, 400, 300, "#1e293b")
|
||||||
|
|
||||||
|
-- Create a complex scene with multiple techniques
|
||||||
|
-- Background gradient effect (simulated with rectangles)
|
||||||
|
for i = 0, 50 do
|
||||||
|
local alpha = i / 50
|
||||||
|
local gray = math.floor(30 + alpha * 40)
|
||||||
|
local grayHex = string.format("#%02x%02x%02x", gray, gray, gray + 20)
|
||||||
|
complexCtx:fillRect(0, i * 6, 400, 6, grayHex)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Animated-looking shapes
|
||||||
|
complexCtx:save()
|
||||||
|
complexCtx:translate(200, 150)
|
||||||
|
|
||||||
|
for i = 1, 8 do
|
||||||
|
complexCtx:save()
|
||||||
|
complexCtx:rotate((i-1) * math.pi / 4)
|
||||||
|
|
||||||
|
-- Gradient-like effect with multiple rectangles
|
||||||
|
for j = 1, 10 do
|
||||||
|
local size = 60 - j * 5
|
||||||
|
local alpha = j / 10
|
||||||
|
local red = math.floor(139 + alpha * 100)
|
||||||
|
local green = math.floor(69 + alpha * 150)
|
||||||
|
local blue = math.floor(19 + alpha * 200)
|
||||||
|
local colorHex = string.format("#%02x%02x%02x", red, green, blue)
|
||||||
|
complexCtx:fillRect(-size/2, 80 - size/2, size, size, colorHex)
|
||||||
|
end
|
||||||
|
|
||||||
|
complexCtx:restore()
|
||||||
|
end
|
||||||
|
|
||||||
|
complexCtx:restore()
|
||||||
|
|
||||||
|
-- Central circle with path
|
||||||
|
complexCtx:setFillStyle("#fbbf24")
|
||||||
|
complexCtx:beginPath()
|
||||||
|
complexCtx:arc(200, 150, 25, 0, 2 * math.pi)
|
||||||
|
complexCtx:fill()
|
||||||
|
|
||||||
|
-- Overlay text
|
||||||
|
complexCtx:setFont("24px sans-serif")
|
||||||
|
complexCtx:drawText(150, 280, "Complex Scene", "#ffffff")
|
||||||
|
|
||||||
|
-- Shader Canvas Test
|
||||||
|
local shaderCanvas = gurt.select("#shader-canvas")
|
||||||
|
local shaderCtx = shaderCanvas:withContext("shader")
|
||||||
|
|
||||||
|
shaderCtx:source([[
|
||||||
|
shader_type canvas_item;
|
||||||
|
|
||||||
|
uniform float time : hint_range(0.0, 10.0) = 1.0;
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
vec2 uv = UV;
|
||||||
|
vec3 color = vec3(0.5 + 0.5 * cos(time + uv.xyx + vec3(0, 2, 4)));
|
||||||
|
COLOR = vec4(color, 1.0);
|
||||||
|
}
|
||||||
|
]])
|
||||||
|
|
||||||
|
gurt.log('Canvas API test completed successfully!')
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>🎨 Complete Canvas API Test Suite</h1>
|
||||||
|
|
||||||
|
<div style="container">
|
||||||
|
<div style="test-section">
|
||||||
|
<h2>📋 API Coverage Test</h2>
|
||||||
|
<p style="description">This page tests every single Canvas 2D API function implemented in GURT:</p>
|
||||||
|
<ul style="text-[#4b5563] text-sm space-y-1 mb-4">
|
||||||
|
<li><strong>Basic Drawing:</strong> fillRect, strokeRect, clearRect, drawCircle, drawText</li>
|
||||||
|
<li><strong>Path Drawing:</strong> beginPath, closePath, moveTo, lineTo, arc, stroke, fill</li>
|
||||||
|
<li><strong>Transformations:</strong> save, restore, translate, rotate, scale</li>
|
||||||
|
<li><strong>Advanced Paths:</strong> quadraticCurveTo, bezierCurveTo</li>
|
||||||
|
<li><strong>Styling:</strong> setStrokeStyle, setFillStyle, setLineWidth, setFont</li>
|
||||||
|
<li><strong>Text Measurement:</strong> measureText</li>
|
||||||
|
<li><strong>Shader Support:</strong> withContext("shader"), source</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="canvas-grid">
|
||||||
|
<!-- Row 1 -->
|
||||||
|
<div style="canvas-item">
|
||||||
|
<h3>Basic Drawing Functions</h3>
|
||||||
|
<p style="description">Rectangle and circle primitives</p>
|
||||||
|
<canvas id="basic-canvas" width="400" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="canvas-item">
|
||||||
|
<h3>Path-Based Drawing</h3>
|
||||||
|
<p style="description">Paths, lines, arcs, and shapes</p>
|
||||||
|
<canvas id="path-canvas" width="400" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 2 -->
|
||||||
|
<div style="canvas-item">
|
||||||
|
<h3>Transformations</h3>
|
||||||
|
<p style="description">Translate, rotate, scale, save/restore</p>
|
||||||
|
<canvas id="transform-canvas" width="400" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="canvas-item">
|
||||||
|
<h3>Curves & Advanced Paths</h3>
|
||||||
|
<p style="description">Quadratic and Bezier curves</p>
|
||||||
|
<canvas id="curve-canvas" width="400" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 3 -->
|
||||||
|
<div style="canvas-item">
|
||||||
|
<h3>Styles & Text</h3>
|
||||||
|
<p style="description">Colors, line widths, fonts, text measurement</p>
|
||||||
|
<canvas id="style-canvas" width="400" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="canvas-item">
|
||||||
|
<h3>Complex Composition</h3>
|
||||||
|
<p style="description">Multiple techniques combined</p>
|
||||||
|
<canvas id="complex-canvas" width="400" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shader Test -->
|
||||||
|
<div style="test-section">
|
||||||
|
<h3>Shader Canvas Support</h3>
|
||||||
|
<p style="description">Custom fragment shader with animated colors</p>
|
||||||
|
<div style="text-center">
|
||||||
|
<canvas id="shader-canvas" width="400" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="bg-[#f0f9ff] border border-[#0ea5e9] p-4 rounded-lg mt-6">
|
||||||
|
<h3 style="text-[#0c4a6e] font-semibold mb-2">✅ Test Results</h3>
|
||||||
|
<p style="text-[#0c4a6e] text-sm">All canvas functions should render correctly above. Check the browser console for any errors.</p>
|
||||||
|
<p style="text-[#0c4a6e] text-sm mt-2">This test demonstrates compatibility with HTML5 Canvas API standards.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
237
tests/snake.html
Normal file
237
tests/snake.html
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
<head>
|
||||||
|
<title>Snake Game - Canvas Demo</title>
|
||||||
|
<icon src="https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Lua-Logo.svg/256px-Lua-Logo.svg.png">
|
||||||
|
<meta name="theme-color" content="#22c55e">
|
||||||
|
<meta name="description" content="Classic Snake game built with GURT Lua API and Canvas">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body { bg-[#1f2937] p-6 }
|
||||||
|
h1 { text-[#22c55e] text-3xl font-bold text-center }
|
||||||
|
.container { bg-[#374151] p-6 rounded-lg shadow-lg max-w-2xl mx-auto }
|
||||||
|
.game-info { bg-[#4b5563] p-4 rounded-lg mb-4 text-white }
|
||||||
|
.score { text-[#fbbf24] text-2xl font-bold }
|
||||||
|
.controls { bg-[#6b7280] p-4 rounded-lg text-[#f3f4f6] }
|
||||||
|
.game-over { bg-[#ef4444] text-white p-4 rounded-lg text-center font-bold }
|
||||||
|
.button { bg-[#22c55e] text-white px-4 py-2 rounded hover:bg-[#16a34a] cursor-pointer }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
-- Game constants
|
||||||
|
local CANVAS_WIDTH = 400
|
||||||
|
local CANVAS_HEIGHT = 400
|
||||||
|
local GRID_SIZE = 20
|
||||||
|
local GRID_WIDTH = CANVAS_WIDTH / GRID_SIZE
|
||||||
|
local GRID_HEIGHT = CANVAS_HEIGHT / GRID_SIZE
|
||||||
|
|
||||||
|
-- Game state
|
||||||
|
local snake = {{x = 10, y = 10}}
|
||||||
|
local direction = {x = 1, y = 0}
|
||||||
|
local food = {x = 15, y = 15}
|
||||||
|
local score = 0
|
||||||
|
local gameRunning = false
|
||||||
|
local gameInterval = nil
|
||||||
|
|
||||||
|
-- Get DOM elements
|
||||||
|
local canvas = gurt.select('#game-canvas')
|
||||||
|
local ctx = canvas:withContext('2d')
|
||||||
|
local scoreDisplay = gurt.select('#score')
|
||||||
|
local gameOverDisplay = gurt.select('#game-over')
|
||||||
|
local startButton = gurt.select('#start-button')
|
||||||
|
|
||||||
|
gameOverDisplay:hide()
|
||||||
|
|
||||||
|
-- Check if position is occupied by snake
|
||||||
|
local function isSnakePosition(x, y)
|
||||||
|
for i = 1, #snake do
|
||||||
|
if snake[i].x == x and snake[i].y == y then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Generate random food position
|
||||||
|
local function generateFood()
|
||||||
|
repeat
|
||||||
|
food.x = math.random(0, GRID_WIDTH - 1)
|
||||||
|
food.y = math.random(0, GRID_HEIGHT - 1)
|
||||||
|
until not isSnakePosition(food.x, food.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Initialize game
|
||||||
|
local function initGame()
|
||||||
|
snake = {{x = 10, y = 10}}
|
||||||
|
direction = {x = 1, y = 0}
|
||||||
|
food = {x = 15, y = 15}
|
||||||
|
score = 0
|
||||||
|
gameRunning = true
|
||||||
|
gameOverDisplay.text = ''
|
||||||
|
gameOverDisplay:hide()
|
||||||
|
scoreDisplay.text = 'Score: 0'
|
||||||
|
generateFood()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gameOver()
|
||||||
|
gameRunning = false
|
||||||
|
clearInterval(gameInterval)
|
||||||
|
gameOverDisplay.text = 'Game Over! Final Score: ' .. score
|
||||||
|
gameOverDisplay:show()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function moveSnake()
|
||||||
|
if not gameRunning then return end
|
||||||
|
|
||||||
|
-- Calculate new head position
|
||||||
|
local head = snake[1]
|
||||||
|
local newHead = {
|
||||||
|
x = head.x + direction.x,
|
||||||
|
y = head.y + direction.y
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Check wall collision
|
||||||
|
if newHead.x < 0 or newHead.x >= GRID_WIDTH or
|
||||||
|
newHead.y < 0 or newHead.y >= GRID_HEIGHT then
|
||||||
|
gameOver()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check self collision
|
||||||
|
if isSnakePosition(newHead.x, newHead.y) then
|
||||||
|
gameOver()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add new head
|
||||||
|
table.insert(snake, 1, newHead)
|
||||||
|
|
||||||
|
-- Check food collision
|
||||||
|
if newHead.x == food.x and newHead.y == food.y then
|
||||||
|
score = score + 10
|
||||||
|
scoreDisplay.text = 'Score: ' .. score
|
||||||
|
generateFood()
|
||||||
|
else
|
||||||
|
-- Remove tail if no food eaten
|
||||||
|
table.remove(snake, #snake)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Render game
|
||||||
|
local function render()
|
||||||
|
-- Clear canvas
|
||||||
|
ctx:fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, '#1f2937')
|
||||||
|
|
||||||
|
-- Draw grid lines
|
||||||
|
for i = 0, GRID_WIDTH do
|
||||||
|
local x = i * GRID_SIZE
|
||||||
|
ctx:strokeRect(x, 0, 1, CANVAS_HEIGHT, '#374151', 1)
|
||||||
|
end
|
||||||
|
for i = 0, GRID_HEIGHT do
|
||||||
|
local y = i * GRID_SIZE
|
||||||
|
ctx:strokeRect(0, y, CANVAS_WIDTH, 1, '#374151', 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Draw snake
|
||||||
|
for i = 1, #snake do
|
||||||
|
local segment = snake[i]
|
||||||
|
local x = segment.x * GRID_SIZE
|
||||||
|
local y = segment.y * GRID_SIZE
|
||||||
|
|
||||||
|
-- Head is brighter green
|
||||||
|
local color = i == 1 and '#22c55e' or '#16a34a'
|
||||||
|
ctx:fillRect(x, y, GRID_SIZE - 2, GRID_SIZE - 2, color)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Draw food
|
||||||
|
local foodX = food.x * GRID_SIZE
|
||||||
|
local foodY = food.y * GRID_SIZE
|
||||||
|
ctx:fillRect(foodX, foodY, GRID_SIZE - 2, GRID_SIZE - 2, '#ef4444')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Game loop
|
||||||
|
local function gameLoop()
|
||||||
|
if gameRunning then
|
||||||
|
moveSnake()
|
||||||
|
render()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Start game
|
||||||
|
local function startGame()
|
||||||
|
if gameInterval then
|
||||||
|
clearInterval(gameInterval)
|
||||||
|
end
|
||||||
|
|
||||||
|
initGame()
|
||||||
|
render()
|
||||||
|
gameInterval = setInterval(gameLoop, 150)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle keyboard input
|
||||||
|
gurt.body:on('keypress', function(event)
|
||||||
|
if not gameRunning then return end
|
||||||
|
|
||||||
|
local key = event.keycode
|
||||||
|
|
||||||
|
-- Arrow keys or WASD
|
||||||
|
if key == 37 or key == 65 then -- Left / A
|
||||||
|
if direction.x ~= 1 then
|
||||||
|
direction = {x = -1, y = 0}
|
||||||
|
end
|
||||||
|
elseif key == 38 or key == 87 then -- Up / W
|
||||||
|
if direction.y ~= 1 then
|
||||||
|
direction = {x = 0, y = -1}
|
||||||
|
end
|
||||||
|
elseif key == 39 or key == 68 then -- Right / D
|
||||||
|
if direction.x ~= -1 then
|
||||||
|
direction = {x = 1, y = 0}
|
||||||
|
end
|
||||||
|
elseif key == 40 or key == 83 then -- Down / S
|
||||||
|
if direction.y ~= -1 then
|
||||||
|
direction = {x = 0, y = 1}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Start button handler
|
||||||
|
startButton:on('click', function()
|
||||||
|
startGame()
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Initialize random seed and render initial state
|
||||||
|
math.randomseed(Time.now())
|
||||||
|
render()
|
||||||
|
|
||||||
|
gurt.log('Snake game initialized!')
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>🐍 Snake Game</h1>
|
||||||
|
|
||||||
|
<div style="container">
|
||||||
|
<div style="game-info">
|
||||||
|
<div style="w-full flex justify-between items-center">
|
||||||
|
<div id="score" style="score">Score: 0</div>
|
||||||
|
<button id="start-button" style="button">Start Game</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-center mb-4">
|
||||||
|
<canvas id="game-canvas" width="400" height="400" style="border border-[#6b7280] rounded-lg bg-[#1f2937]"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="game-over" style="game-over mb-4">Test</div>
|
||||||
|
|
||||||
|
<div style="controls">
|
||||||
|
<h3 style="text-[#f9fafb] font-semibold mb-2">Controls:</h3>
|
||||||
|
<p style="text-sm">Use <strong>Arrow Keys</strong> or <strong>WASD</strong> to control the snake</p>
|
||||||
|
<ul style="text-sm mt-2 space-y-1">
|
||||||
|
<li>🔼 Up: Arrow Up or W</li>
|
||||||
|
<li>🔽 Down: Arrow Down or S</li>
|
||||||
|
<li>◀️ Left: Arrow Left or A</li>
|
||||||
|
<li>▶️ Right: Arrow Right or D</li>
|
||||||
|
</ul>
|
||||||
|
<p style="text-xs text-[#d1d5db] mt-3">Eat the red food to grow and increase your score!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
Reference in New Issue
Block a user