class CanvasWidget extends BaseWidget { $el; #data = []; #scale = 1; #resize_h; constructor(data, renderer) { super(data, renderer); this.makeLayout({ type: 'div', class: 'w_canvas', children: [ { type: 'canvas', name: 'el', events: { click: e => this.#click(e) } } ] }); this.#resize(); this.#resize_h = this.#resize.bind(this); window.addEventListener('resize', this.#resize_h); wait2Frame().then(() => { this.#resize(); }); this.update(data); this.disable(this.$el, data.disable); } update(data) { super.update(data); if ('active' in data) this.$el.style.cursor = data.active ? 'pointer' : ''; if ('data' in data) { this.#data = this.#data.concat(data.data); this.#show(data.data); } } close() { window.removeEventListener('resize', this.#resize_h); } #click(e) { if (!this.data.active) return; const rect = this.$el.getBoundingClientRect(); const ratio = window.devicePixelRatio; let x = Math.round((e.clientX - rect.left) / this.#scale * ratio); if (x < 0) x = 0; let y = Math.round((e.clientY - rect.top) / this.#scale * ratio); if (y < 0) y = 0; this.set((x << 16) | y); this.setSuffix('[' + x + ',' + y + ']'); } #resize() { const rw = this.$el.parentNode.clientWidth; if (!rw) return; const scale = rw / this.data.width; const ratio = window.devicePixelRatio; this.#scale = scale * ratio; const rh = Math.floor(this.data.height * scale); this.$el.style.width = rw + 'px'; this.$el.style.height = rh + 'px'; this.$el.width = Math.floor(rw * ratio); this.$el.height = Math.floor(rh * ratio); this.#show(this.#data); } #show(data = null) { if (!this.$el.parentNode.clientWidth) return; if (!data) data = this.#data; const cv = this.$el; const cx = cv.getContext("2d"); const cmd_list = ['fillStyle', 'strokeStyle', 'shadowColor', 'shadowBlur', 'shadowOffsetX', 'shadowOffsetY', 'lineWidth', 'miterLimit', 'font', 'textAlign', 'textBaseline', 'lineCap', 'lineJoin', 'globalCompositeOperation', 'globalAlpha', 'scale', 'rotate', 'rect', 'fillRect', 'strokeRect', 'clearRect', 'moveTo', 'lineTo', 'quadraticCurveTo', 'bezierCurveTo', 'translate', 'arcTo', 'arc', 'fillText', 'strokeText', 'drawImage', 'roundRect', 'fill', 'stroke', 'beginPath', 'closePath', 'clip', 'save', 'restore']; const const_list = ['butt', 'round', 'square', 'square', 'bevel', 'miter', 'start', 'end', 'center', 'left', 'right', 'alphabetic', 'top', 'hanging', 'middle', 'ideographic', 'bottom', 'source-over', 'source-atop', 'source-in', 'source-out', 'destination-over', 'destination-atop', 'destination-in', 'destination-out', 'lighter', 'copy', 'xor', 'top', 'bottom', 'middle', 'alphabetic']; const cv_map = (v, h) => { v *= this.#scale; return v >= 0 ? v : (h ? cv.height : cv.width) - v; } for (const item of data) { let [cmdName, ...args] = ("" + item).split(':'); if (args.length === 1) args.push(...args.pop().split(',')); const cmd = parseInt(cmdName, 10); args = args.map(v => { if (v.match(/^-?\d+$/)) return parseInt(v); else if (v.match(/^\d+\.\d+$/)) return parseFloat(v); else return v; }) if (!isNaN(cmd) && cmd <= cmd_list.length) { cmdName = cmd_list[cmd]; if (cmd <= 2) args[0] = intToColA(args[0]); // shadowColor else if (cmd <= 7) args[0] *= this.#scale; // miterLimit else if (cmd <= 8) args[0] = Number(args[0].split('px')[0]) * this.#scale + 'px' + args[0].split('px')[1]; // font else if (cmd <= 13) args[0] = const_list[args[0]]; // globalCompositeOperation else if (cmd <= 14); // globalAlpha else if (cmd <= 16); // rotate else if (cmd <= 26) { // arcTo args = args.map((v, i) => cv_map(v, i % 2)) } else if (cmd == 27) { // arc args = [cv_map(args[0], 0), cv_map(args[1], 1), cv_map(args[2], 0), args[3], args[4], args[5]]; } else if (cmd <= 29) { // strokeText if (args[3]) args = [args[0], cv_map(args[1], 0), cv_map(args[2], 1), args[3]]; else args = [args[0], cv_map(args[1], 0), cv_map(args[2], 1)]; } else if (cmd == 30) { // drawImage let img = new Image(); for (let i in args) { if (i > 0) args[i] = cv_map(args[i], !(i % 2)); } if (args[0].startsWith('http://') || args[0].startsWith('https://')) { img.src = args[0]; } else { this.addFile(args[0], 'url', (file) => { img.src = file; }); } img.onload = function () { args[0] = img; cx.drawImage(...args); } continue; } else if (cmd == 31) { // roundRect for (let i = 0; i < 4; i++) { args[i] = cv_map(args[i], i % 2); } if (args.length == 5) args[4] *= this.#scale; else args.push(args.slice()); } } try { if (!args.length) { cx[cmdName].call(cx); } else { const fn = cx[cmdName]; if (typeof fn === 'function') fn.apply(cx, args); else cx[cmdName] = args[0]; } } catch (e) { console.log(e); } } } } Renderer.register('canvas', CanvasWidget); function intToColA(val) { if (val === null || val === undefined) return null; return "#" + Number(val).toString(16).padStart(8, '0'); }