merge new_starr_app@qas history into apps/web

git-subtree-dir: apps/web
git-subtree-mainline: eaa6d2276a
git-subtree-split: 459af9d46a
This commit is contained in:
2026-08-24 12:21:16 +08:00
699 changed files with 103969 additions and 0 deletions
+198
View File
@@ -0,0 +1,198 @@
module SketchToolbar = {
@module("./SketchToolbar.jsx") @react.component
external make: (
~state: UseSketch.sketchState,
~onSetTool: UseSketch.tool => unit,
~onSetColor: UseSketch.color => unit,
~onClear: unit => unit,
~onSave: unit => unit,
~onClose: unit => unit,
) => React.element = "default"
}
type point = {x: float, y: float}
@val external document: {..} = "document"
external asCanvas: Dom.element => {..} = "%identity"
@react.component
let make = (
~isActive: bool,
~tool: UseSketch.tool,
~color: UseSketch.color,
~onSave: unit => unit,
~onClear: unit => unit,
~onClose: unit => unit,
~onSetTool: UseSketch.tool => unit,
~onSetColor: UseSketch.color => unit,
) => {
let canvasRef: React.ref<Nullable.t<Dom.element>> = React.useRef(Nullable.null)
let isDrawing = React.useRef(false)
let lastPoint = React.useRef(None)
React.useEffect1(() => {
switch canvasRef.current->Nullable.toOption {
| None => ()
| Some(el) =>
let canvas = asCanvas(el)
let parent = canvas["parentElement"]
canvas["width"] = parent["offsetWidth"]
canvas["height"] = parent["offsetHeight"]
}
None
}, [isActive])
let getPos = (e: JsxEvent.Mouse.t, canvas) => {
let rect = canvas["getBoundingClientRect"]()
{
x: Float.fromInt(JsxEvent.Mouse.clientX(e)) -. rect["left"],
y: Float.fromInt(JsxEvent.Mouse.clientY(e)) -. rect["top"],
}
}
let startDraw = (e: JsxEvent.Mouse.t) => {
switch canvasRef.current->Nullable.toOption {
| None => ()
| Some(el) =>
let canvas = asCanvas(el)
isDrawing.current = true
let pos = getPos(e, canvas)
lastPoint.current = Some(pos)
if tool === UseSketch.Highlighter {
let ctx = canvas["getContext"]("2d")
let hex = UseSketch.colorToHex(color)
let width = UseSketch.toolToWidth(tool, 2.0)
ctx["globalAlpha"] = 0.25
ctx["globalCompositeOperation"] = "source-over"
ctx["strokeStyle"] = hex
ctx["lineWidth"] = width
ctx["lineCap"] = "round"
ctx["lineJoin"] = "round"
let _ = ctx["beginPath"]()
let _ = ctx["moveTo"](pos.x, pos.y)
} else if tool === UseSketch.Text {
let text = canvas["ownerDocument"]["defaultView"]["prompt"]("Enter text:", "")
switch Nullable.toOption(text) {
| None => ()
| Some(t) =>
let ctx = canvas["getContext"]("2d")
let hex = UseSketch.colorToHex(color)
ctx["globalAlpha"] = 1.0
ctx["fillStyle"] = hex
ctx["font"] = "bold 16px sans-serif"
let _ = ctx["fillText"](t, pos.x, pos.y)
}
}
}
}
let draw = (e: JsxEvent.Mouse.t) => {
if isDrawing.current && tool !== UseSketch.Text {
switch canvasRef.current->Nullable.toOption {
| None => ()
| Some(el) =>
let canvas = asCanvas(el)
switch lastPoint.current {
| None => ()
| Some(last) =>
let ctx = canvas["getContext"]("2d")
let pos = getPos(e, canvas)
let hex = UseSketch.colorToHex(color)
let width = UseSketch.toolToWidth(tool, 2.0)
switch tool {
| UseSketch.Highlighter =>
let _ = ctx["lineTo"](pos.x, pos.y)
let _ = ctx["stroke"]()
| _ =>
ctx["globalAlpha"] = 1.0
ctx["strokeStyle"] = hex
ctx["lineWidth"] = width
ctx["lineCap"] = "round"
ctx["lineJoin"] = "round"
let _ = ctx["beginPath"]()
let _ = ctx["moveTo"](last.x, last.y)
let _ = ctx["quadraticCurveTo"](
last.x +. (pos.x -. last.x) /. 2.0,
last.y +. (pos.y -. last.y) /. 2.0,
pos.x,
pos.y,
)
let _ = ctx["stroke"]()
}
lastPoint.current = Some(pos)
}
}
}
}
let stopDraw = (_e: JsxEvent.Mouse.t) => {
if tool === UseSketch.Highlighter {
switch canvasRef.current->Nullable.toOption {
| None => ()
| Some(el) =>
let canvas = asCanvas(el)
let ctx = canvas["getContext"]("2d")
let _ = ctx["stroke"]()
ctx["globalAlpha"] = 1.0
ctx["globalCompositeOperation"] = "source-over"
}
}
isDrawing.current = false
lastPoint.current = None
}
let handleClear = () => {
switch canvasRef.current->Nullable.toOption {
| None => ()
| Some(el) =>
let canvas = asCanvas(el)
let ctx = canvas["getContext"]("2d")
let _ = ctx["clearRect"](0, 0, canvas["width"], canvas["height"])
onClear()
}
}
let handleSave = () => {
switch canvasRef.current->Nullable.toOption {
| None => ()
| Some(el) =>
let canvas = asCanvas(el)
let dataUrl = canvas["toDataURL"]("image/png")
let link = document["createElement"]("a")
link["href"] = dataUrl
link["download"] = "sketch.png"
let _ = link["click"]()
onSave()
}
}
if !isActive {
React.null
} else {
<div className="absolute inset-0 z-10" style={{pointerEvents: isActive ? "all" : "none"}}>
<SketchToolbar.make
state={{
UseSketch.isActive,
tool,
color,
strokeWidth: 2.0,
}}
onSetTool={onSetTool}
onSetColor={onSetColor}
onClear={handleClear}
onSave={handleSave}
onClose={onClose}
/>
<canvas
ref={canvasRef->ReactDOM.Ref.domRef}
width="100%"
height="100%"
className="w-full h-full cursor-crosshair"
onMouseDown={startDraw}
onMouseMove={draw}
onMouseUp={stopDraw}
onMouseLeave={stopDraw}
/>
</div>
}
}