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