You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

166 lines
6.4 KiB
JavaScript

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');
}