import { Controller } from "@hotwired/stimulus" import Konva from "konva" // Draws 2D book characters using Konva.js // Shapes are draggable; right-click removes them. export default class extends Controller { static targets = ["container", "data"] static values = { initial: String } connect() { this._initStage() if (this.initialValue) this._load(this.initialValue) this._bindAutoSave() } disconnect() { if (this.stage) this.stage.destroy() } // ── Tools ───────────────────────────────────────────────── addHead() { this._add(new Konva.Circle({ x: 190, y: 90, radius: 55, fill: "#f5d5a0", stroke: "#8b6914", strokeWidth: 2, draggable: true, name: "shape" })) } addBody() { this._add(new Konva.Rect({ x: 145, y: 155, width: 90, height: 120, fill: "#8b4513", stroke: "#5a2d0c", strokeWidth: 2, cornerRadius: 6, draggable: true, name: "shape" })) } addEye() { const g = new Konva.Group({ x: 175, y: 80, draggable: true, name: "shape" }) g.add(new Konva.Circle({ x: 0, y: 0, radius: 8, fill: "#fff", stroke: "#333", strokeWidth: 1 })) g.add(new Konva.Circle({ x: 0, y: 0, radius: 4, fill: "#1a0a00" })) g.add(new Konva.Circle({ x: 2, y: -2, radius: 1.5, fill: "#fff" })) this._add(g) } addMouth() { this._add(new Konva.Arc({ x: 190, y: 120, innerRadius: 18, outerRadius: 22, angle: 180, rotation: 0, fill: "#c0392b", stroke: "#7b241c", strokeWidth: 1, draggable: true, name: "shape" })) } addHair() { this._add(new Konva.Ellipse({ x: 190, y: 42, radiusX: 60, radiusY: 28, fill: "#4a2c0a", stroke: "#2c1a05", strokeWidth: 1, draggable: true, name: "shape" })) } addArm() { this._add(new Konva.Rect({ x: 110, y: 160, width: 28, height: 80, fill: "#8b4513", stroke: "#5a2d0c", strokeWidth: 1, cornerRadius: 8, draggable: true, name: "shape", rotation: -8 })) } addLeg() { this._add(new Konva.Rect({ x: 165, y: 275, width: 32, height: 100, fill: "#2c3e50", stroke: "#1a252f", strokeWidth: 1, cornerRadius: 6, draggable: true, name: "shape" })) } addAccessory() { // A simple star / magic wand this._add(new Konva.Star({ x: 240, y: 80, numPoints: 5, innerRadius: 12, outerRadius: 24, fill: "#f1c40f", stroke: "#c9a227", strokeWidth: 1.5, draggable: true, name: "shape" })) } clear() { if (!confirm("Очистить холст?")) return this.layer.destroyChildren() this._save() this.layer.draw() } // ── Private ──────────────────────────────────────────────── _initStage() { const w = this.containerTarget.offsetWidth || 380 const h = this.containerTarget.offsetHeight || 480 this.stage = new Konva.Stage({ container: this.containerTarget, width: w, height: h }) this.layer = new Konva.Layer() this.stage.add(this.layer) // Subtle parchment grid const bg = new Konva.Rect({ x: 0, y: 0, width: w, height: h, fill: "#f0e4c8", listening: false }) this.layer.add(bg) this.layer.draw() } _add(shape) { shape.on("contextmenu", (e) => { e.evt.preventDefault() shape.destroy() this._save() this.layer.draw() }) this.layer.add(shape) this.layer.draw() this._save() } _bindAutoSave() { this.layer.on("dragend", () => this._save()) } _save() { if (this.hasDataTarget) { this.dataTarget.value = JSON.stringify(this.layer.toJSON()) } } _load(json) { try { const data = JSON.parse(json) if (data && data.children) { data.children.forEach(child => { let shape switch (child.className) { case "Circle": shape = new Konva.Circle(child.attrs); break case "Rect": shape = new Konva.Rect(child.attrs); break case "Ellipse": shape = new Konva.Ellipse(child.attrs); break case "Arc": shape = new Konva.Arc(child.attrs); break case "Star": shape = new Konva.Star(child.attrs); break case "Group": shape = this._buildGroup(child); break default: return } if (shape) { shape.draggable(true) shape.on("contextmenu", (e) => { e.evt.preventDefault() shape.destroy() this._save() this.layer.draw() }) this.layer.add(shape) } }) this.layer.draw() } } catch (e) { console.warn("[canvas] failed to load saved data", e) } } _buildGroup(data) { const g = new Konva.Group(data.attrs) if (data.children) { data.children.forEach(child => { let shape switch (child.className) { case "Circle": shape = new Konva.Circle(child.attrs); break case "Rect": shape = new Konva.Rect(child.attrs); break default: return } if (shape) g.add(shape) }) } return g } }