UNPKG

drawable-js

Version:

A lightweight JavaScript library to draw shapes and effects on HTML canvas, inspired by Android's Drawable.

807 lines (790 loc) 28.4 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.DrawableJS = {})); })(this, (function (exports) { 'use strict'; function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e) { t && (r = t); var n = 0, F = function () {}; return { s: F, n: function () { return n >= r.length ? { done: true } : { done: false, value: r[n++] }; }, e: function (r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = true, u = false; return { s: function () { t = t.call(r); }, n: function () { var r = t.next(); return a = r.done, r; }, e: function (r) { u = true, o = r; }, f: function () { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } /* * DrawableJS * Copyright (c) 2025 jaianper * * This file is part of DrawableJS. * Licensed under the GNU General Public License v3.0. * You may obtain a copy of the License at https://www.gnu.org/licenses/gpl-3.0.txt */ // --- Exportable constants --- var Shape = { LINE: 'LINE', OVAL: 'OVAL', RECTANGLE: 'RECT' }; var Gradient = { RADIAL_GRADIENT: 'R_G', LINEAR_GRADIENT: 'L_G' }; var Alignment = { TOP: 'A_T', BOTTOM: 'A_B', LEFT: 'A_L', RIGHT: 'A_R', CENTER: 'A_C' }; var Orientation = { VERTICAL: 'O_V', HORIZONTAL: 'O_H' }; var DAR = 0.75; // DEFAULT_ASCENT_RATIO var DDR = 0.03; // DEFAULT_DESCENT_RATIO /** * `ascentRatio` and `descentRatio` are empirical estimates of typographic layout proportions that are used * as a fallback when the browser does not support the `actualBoundingBoxAscent` and `actualBoundingBoxDescent` properties. * * - 75% of the font size is usually above the baseline (ascent). * - 3% is usually below (descent). */ var Font = { ARIAL: { name: 'Arial', ascentRatio: 0.78, descentRatio: DDR }, AVERIA: { name: 'Averia Gruesa', ascentRatio: 0.76, descentRatio: DDR }, BALOO: { name: 'Baloo', ascentRatio: 0.69, descentRatio: DDR }, BEBAS_NEUE: { name: 'Bebas Neue', ascentRatio: DAR, descentRatio: DDR }, BUTCHERMAN: { name: 'Butcherman', ascentRatio: 0.77, descentRatio: 0.05 }, CALIBRI: { name: 'Calibri', ascentRatio: 0.69, descentRatio: DDR }, COMIC_SANS: { name: 'Comic Sans MS', ascentRatio: 0.78, descentRatio: DDR }, CONSOLAS: { name: 'Consolas', ascentRatio: 0.69, descentRatio: DDR }, COURIER_NEW: { name: 'Courier New', ascentRatio: 0.65, descentRatio: DDR }, CRASH_NUMB: { name: 'Crash Numbering Serif', ascentRatio: 0.74, descentRatio: DDR }, CREEPSTER: { name: 'Creepster', ascentRatio: 0.76, descentRatio: DDR }, DROID_SANS: { name: 'Droid Sans', ascentRatio: 0.73, descentRatio: DDR }, DROID_SERIF: { name: 'Droid Serif', ascentRatio: 0.73, descentRatio: DDR }, EATER: { name: 'Eater', ascentRatio: 0.85, descentRatio: 0.04 }, FLAVORS: { name: 'Flavors', ascentRatio: 0.78, descentRatio: DDR }, FREDOKA_ONE: { name: 'Fredoka One', ascentRatio: 0.76, descentRatio: DDR }, GEORGIA: { name: 'Georgia', ascentRatio: DAR, descentRatio: DDR }, GOCHI_HAND: { name: 'Gochi Hand', ascentRatio: 0.59, descentRatio: DDR }, IMPACT: { name: 'Impact', ascentRatio: 0.83, descentRatio: DDR }, MOUNT_CHRIST: { name: 'Mountains of Christmas', ascentRatio: 0.83, descentRatio: 0.06 }, RAMABHADRA: { name: 'Ramabhadra', ascentRatio: 0.76, descentRatio: DDR }, TIMES_NEW_R: { name: 'Times New Roman', ascentRatio: DAR, descentRatio: DDR }, TREBUCHET: { name: 'Trebuchet MS', ascentRatio: DAR, descentRatio: DDR }, VERDANA: { name: 'Verdana', ascentRatio: 0.77, descentRatio: DDR }, values: function values() { return Object.values(this).filter(function (f) { return typeof f !== 'function'; }); }, getFonts: function getFonts() { return this.values().map(function (f) { return f.name; }); }, keys: function keys() { var _this = this; return Object.keys(this).filter(function (k) { return typeof _this[k] !== 'function'; }); }, getKeyByName: function getKeyByName(name) { var _this2 = this; return this.keys().find(function (k) { return _this2[k].name === name; }); } }; var FontLoader = { load: function load(info) { var fonts = info.families; var width = 150, height = 25; var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; var _loadFont = function loadFont(font) { var text = new Drawable(width, height); text.ctx = canvas.getContext('2d'); text.update = function () { this.items = [{ text: 'AEIOUBCDFGHIJ/0123456789', font: { family: font, size: 20 }, color: '#FFFFFF', textAlign: 'left', verticalAlign: 'top', x: 0, y: 0 }]; }; text.onPostBuild = function () { if (fonts.length > 0) _loadFont(fonts.pop());else info.active(); }; text.build(); }; _loadFont(fonts.pop()); } }; // --- Internal functions --- var getPointsFromAngle = function getPointsFromAngle(angle, center, width, height) { // Convert center of % to decimal values var centerXPercent = center !== null && center !== void 0 && center.x ? parseFloat(center.x.replace('%', '')) / 100 : 0; var centerYPercent = center !== null && center !== void 0 && center.y ? parseFloat(center.y.replace('%', '')) / 100 : 0; var halfWidth = width / 2; var halfHeight = height / 2; // Critical angle separating width/height in the circle var fovAngle = Math.atan(halfHeight / halfWidth) * (180 / Math.PI); var altFovAngle = 90 - fovAngle; // Ensure angle within 0–360° var normalizedAngle = angle % 360; // Determine quadrant and relative angle var quadrant = 1; var angleInQuadrant = normalizedAngle; if (normalizedAngle >= 270) { angleInQuadrant = normalizedAngle - 270; quadrant = 4; } else if (normalizedAngle >= 180) { angleInQuadrant = normalizedAngle - 180; quadrant = 3; } else if (normalizedAngle >= 90) { angleInQuadrant = normalizedAngle - 90; quadrant = 2; } // Determine adjacent side and acute angle (to be used with tangent) var adjacent, innerAngleDeg; var inQuadrant13 = quadrant === 1 || quadrant === 3; if (inQuadrant13) { if (angleInQuadrant < fovAngle) { adjacent = halfWidth; innerAngleDeg = angleInQuadrant; } else { adjacent = halfHeight; innerAngleDeg = 90 - angleInQuadrant; } } else { if (angleInQuadrant < altFovAngle) { adjacent = halfHeight; innerAngleDeg = angleInQuadrant; } else { adjacent = halfWidth; innerAngleDeg = 90 - angleInQuadrant; } } var opposite = Math.tan(innerAngleDeg * (Math.PI / 180)) * adjacent; // Coordinates of a symmetrical line with respect to the center var x0, y0, x1, y1; switch (quadrant) { case 1: if (angleInQuadrant < fovAngle) { x0 = halfWidth - adjacent; y0 = halfHeight + opposite; x1 = halfWidth + adjacent; y1 = halfHeight - opposite; } else { x0 = halfWidth - opposite; y0 = halfHeight + adjacent; x1 = halfWidth + opposite; y1 = halfHeight - adjacent; } x0 += width * centerXPercent; y0 -= height * centerYPercent; break; case 2: if (angleInQuadrant < altFovAngle) { x0 = halfWidth + opposite; y0 = halfHeight + adjacent; x1 = halfWidth - opposite; y1 = halfHeight - adjacent; } else { x0 = halfWidth + adjacent; y0 = halfHeight + opposite; x1 = halfWidth - adjacent; y1 = halfHeight - opposite; } x0 -= width * centerXPercent; y0 -= height * centerYPercent; break; case 3: if (angleInQuadrant < fovAngle) { x0 = halfWidth + adjacent; y0 = halfHeight - opposite; x1 = halfWidth - adjacent; y1 = halfHeight + opposite; } else { x0 = halfWidth + opposite; y0 = halfHeight - adjacent; x1 = halfWidth - opposite; y1 = halfHeight + adjacent; } x0 -= width * centerXPercent; y0 += height * centerYPercent; break; case 4: if (angleInQuadrant < altFovAngle) { x0 = halfWidth - opposite; y0 = halfHeight - adjacent; x1 = halfWidth + opposite; y1 = halfHeight + adjacent; } else { x0 = halfWidth - adjacent; y0 = halfHeight - opposite; x1 = halfWidth + adjacent; y1 = halfHeight + opposite; } x0 += width * centerXPercent; y0 += height * centerYPercent; break; } return { x0: x0, y0: y0, x1: x1, y1: y1 }; }; var ImageCache = {}; var getImgCopy = function getImgCopy(imgName, width, height) { var imgSize = width + '_' + height; var targetImages = ImageCache[imgSize]; var copy; if (targetImages && targetImages[imgName]) { copy = targetImages[imgName]; } return copy; }; var drawable2Image = function drawable2Image(drawable, type, callback) { var canvas = document.createElement('canvas'); canvas.width = drawable.width; canvas.height = drawable.height; drawable.ctx = canvas.getContext('2d'); drawable.build(); var img = new Image(); if (typeof callback !== 'undefined') { img.onload = callback; } img.src = canvas.toDataURL(type); return img; }; // --- Clase principal --- function Drawable() { var _this3 = this; var width = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; var height = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; var x = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; var y = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; this.margin = { top: 0, left: 0, bottom: 0, right: 0 }; this.width = width; this.height = height; this.x = x; this.y = y; var tWidth = 0, tHeight = 0; var applyFilter = function applyFilter(imgInfo) { var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); canvas.width = imgInfo.w || imgInfo.width; canvas.height = imgInfo.h || imgInfo.height; /*if(typeof imgInfo.angle !== 'undefined') { ctx.save(); ctx.translate(canvas.width/2, canvas.height/2); ctx.rotate(imgInfo.angle * (Math.PI / 180)); ctx.drawImage(imgInfo.image, -canvas.width / 2, -canvas.height / 2); } else {*/ ctx.drawImage(imgInfo.image, 0, 0, canvas.width, canvas.height); //} ctx.globalCompositeOperation = 'source-atop'; ctx.fillStyle = imgInfo.cf || imgInfo.colorFilter; ctx.fillRect(0, 0, canvas.width, canvas.height); /*if(typeof imgInfo.angle !== 'undefined') { ctx.restore(); }*/ return canvas; }; var removeShadow = function removeShadow() { _this3.ctx.shadowOffsetX = 0; _this3.ctx.shadowOffsetY = 0; _this3.ctx.shadowColor = 'transparent'; _this3.ctx.shadowBlur = 0; }; var addShadow = function addShadow(shadow) { _this3.ctx.shadowOffsetX = shadow.dx; _this3.ctx.shadowOffsetY = shadow.dy; _this3.ctx.shadowColor = shadow.color; _this3.ctx.shadowBlur = shadow.blur; }; var draw = function draw() { var _iterator = _createForOfIteratorHelper(_this3.items), _step; try { var _loop = function _loop() { var item = _step.value; var cStroke = false; if (typeof item.angle !== 'undefined') { _this3.ctx.save(); _this3.ctx.translate(item.x + item.width / 2, item.y + item.height / 2); _this3.ctx.rotate(item.angle * (Math.PI / 180)); // degrees to radians } if (typeof item.color !== 'undefined') { _this3.ctx.fillStyle = item.color; } else if (typeof item.gradient !== 'undefined') { var gradient = item.gradient; var grd; if (gradient.type == Gradient.RADIAL_GRADIENT) { grd = _this3.ctx.createRadialGradient(gradient.center1.x, gradient.center1.y, gradient.radius1, gradient.center2.x, gradient.center2.y, gradient.radius2); } else if (gradient.type == Gradient.LINEAR_GRADIENT) { points = getPointsFromAngle(gradient.angle, gradient.center, item.width, item.height); grd = _this3.ctx.createLinearGradient(points.x0 + item.x, points.y0 + item.y, points.x1 + item.x, points.y1 + item.y); } grd.addColorStop(0, gradient.startColor); if (typeof gradient.centerColor !== 'undefined') { grd.addColorStop(0.5, gradient.centerColor); } grd.addColorStop(1, gradient.endColor); _this3.ctx.fillStyle = grd; } if (typeof item.shadow !== 'undefined') { addShadow(item.shadow); } if (typeof item.text !== 'undefined') { // item.y indicates the position from where the text begins to be painted // this.y indicates the position reserved to start erasing the text, given that some characters overflow and also because of shadows. var fontFamily = item.font.family.name || 'Arial'; var fontStyle = item.font.style ? "".concat(item.font.style, " ") : ''; var fontSize = item.font.size || 12; var ascentRatio = item.font.family.ascentRatio || DAR; var descentRatio = item.font.family.descentRatio || DDR; var shadowBlur = typeof item.shadow !== 'undefined' ? item.shadow.blur : 0; // Set the font _this3.ctx.font = "".concat(fontStyle).concat(fontSize, "px ").concat(fontFamily); _this3.ctx.textBaseline = 'alphabetic'; // Consistent baseline for any source and on any device. var metrics = _this3.ctx.measureText(item.text); // Empirical estimates are used // ascent: the distance from the baseline of the text to the highest part of the letters (e.g., the top of a "d"). // descent: distance from the baseline to the lowest point (e.g., the tail of a "g" or "p"). var ascent = fontSize * ascentRatio; var descent = fontSize * descentRatio; // `actualBoundingBoxAscent` and `actualBoundingBoxDescent` do not give exact values. //const ascent = metrics.actualBoundingBoxAscent || ascent; //const descent = metrics.actualBoundingBoxDescent || descent; var halfAscent = ascent / 2; var adjustedY = item.y - descent; var mY; if (typeof item.lineHeight !== 'undefined') adjustedY -= item.lineHeight; switch (item.verticalAlign) { case "top": adjustedY -= shadowBlur; mY = adjustedY; adjustedY += ascent; break; case "middle": mY = adjustedY - halfAscent; adjustedY += halfAscent - descent / 2; break; case "bottom": mY = adjustedY - ascent; break; default: adjustedY -= shadowBlur; mY = adjustedY; adjustedY += ascent; } _this3.x = item.x - halfAscent; _this3.y = mY - shadowBlur - halfAscent; _this3.ctx.textAlign = item.textAlign; _this3.ctx.fillText(item.text, item.x, adjustedY); tWidth = metrics.width + ascent; tHeight = ascent + descent + shadowBlur * 2; } else if (item.shape == Shape.LINE) { _this3.ctx.beginPath(); _this3.ctx.moveTo(item.from.x, item.from.y); _this3.ctx.lineTo(item.to.x, item.to.y); } else if (item.shape == Shape.OVAL) { _this3.ctx.beginPath(); _this3.ctx.arc(item.x, item.y, item.radius, 0, Math.PI * 2); _this3.ctx.closePath(); _this3.ctx.fill(); } else if (item.shape == Shape.RECTANGLE) { var rx = item.x; var ry = item.y; var rw = item.width; var rh = item.height; if (typeof item.cornerRadius !== 'undefined') { var cr0 = item.cornerRadius[0] || 0; var cr1 = item.cornerRadius[1] || 0; var cr2 = item.cornerRadius[2] || 0; var cr3 = item.cornerRadius[3] || 0; _this3.ctx.beginPath(); _this3.ctx.moveTo(rx + rw - cr1, ry); _this3.ctx.arcTo(rx + rw, ry, rx + rw, ry + cr1, cr1); _this3.ctx.lineTo(rx + rw, ry + rh - cr2); _this3.ctx.arcTo(rx + rw, ry + rh, rx + rw - cr2, ry + rh, cr2); _this3.ctx.lineTo(rx + cr3, ry + rh); _this3.ctx.arcTo(rx, ry + rh, rx, ry + rh - cr3, cr3); _this3.ctx.lineTo(rx, ry + cr0); _this3.ctx.arcTo(rx, ry, rx + cr0, ry, cr0); _this3.ctx.closePath(); _this3.ctx.fill(); } else { _this3.ctx.fillRect(rx, ry, rw, rh); if (typeof item.stroke !== 'undefined') { _this3.ctx.lineWidth = item.stroke.width; _this3.ctx.strokeStyle = item.stroke.color; _this3.ctx.strokeRect(rx, ry, rw, rh); cStroke = true; } } } else if (typeof item.image !== 'undefined') { if (item.image == 'image/url') { imgUrl = item.url; imgName = imgUrl.substring(imgUrl.lastIndexOf('/') + 1); imgCopy = getImgCopy(imgName, item.width, item.height); if (imgCopy) { //console.log('cache'); if (typeof imgCopy.w !== 'undefined' && typeof imgCopy.h !== 'undefined') { var posX = item.x; var posY = item.y; if (typeof item.angle !== 'undefined') { posX = -imgCopy.w / 2; posY = -imgCopy.h / 2; } if (typeof item.colorFilter !== 'undefined') { imgCopy.cf = item.colorFilter; _this3.ctx.drawImage(applyFilter(imgCopy), posX, posY); } else { _this3.ctx.drawImage(imgCopy.image, posX, posY, imgCopy.w, imgCopy.h); } } else { _this3.ctx.drawImage(imgCopy.image, item.x, item.y); } if (typeof item.onLoadImg !== 'undefined') item.onLoadImg(); } else { //console.log('new'); var img = new Image(); img.onload = function (n, w, h) { if (typeof item.angle !== 'undefined') { this.ctx.save(); this.ctx.translate(item.x + item.width / 2, item.y + item.height / 2); this.ctx.rotate(item.angle * (Math.PI / 180)); // degrees to radians } var imgInfo = { w: w, h: h }; var imgSize = w + '_' + h; if (typeof w !== 'undefined' && typeof h !== 'undefined') { var _posX = item.x; var _posY = item.y; if (typeof item.angle !== 'undefined') { _posX = -w / 2; _posY = -h / 2; } if (typeof item.colorFilter !== 'undefined') { item.image = img; imgInfo.cf = item.colorFilter; this.ctx.drawImage(applyFilter(item), _posX, _posY); } else { this.ctx.drawImage(img, _posX, _posY, w, h); } imgInfo.image = img; } else { imgInfo.image = img; this.ctx.drawImage(img, item.x, item.y); } if (typeof item.angle !== 'undefined') { this.ctx.restore(); } if (typeof ImageCache[imgSize] === 'undefined') ImageCache[imgSize] = {}; ImageCache[imgSize][n] = imgInfo; if (item.onLoadImg) item.onLoadImg(); }.bind(_this3, imgName, item.width, item.height); img.src = item.url; /*const img = await loadImage(item.url); if(typeof item.angle !== 'undefined') { this.ctx.save(); this.ctx.translate(item.x + (item.width/2), item.y + (item.height/2)); this.ctx.rotate(item.angle * (Math.PI / 180)); // degrees to radians } var imgInfo = { w: item.width, h: item.height }; var imgSize = item.width + '_' + item.height; if (typeof item.width !== 'undefined' && typeof item.height !== 'undefined') { let posX = item.x; let posY = item.y; if(typeof item.angle !== 'undefined') { posX = -item.width/2; posY = -item.width/2; } if (typeof item.colorFilter !== 'undefined') { item.image = img; imgInfo.cf = item.colorFilter; this.ctx.drawImage(applyFilter(item), posX, posY); } else { this.ctx.drawImage(img, posX, posY, item.width, item.height); } imgInfo.image = img; } else { imgInfo.image = img; this.ctx.drawImage(img, item.x, item.y); } if(typeof item.angle !== 'undefined') { this.ctx.restore(); } if (typeof ImageCache[imgSize] === 'undefined') ImageCache[imgSize] = {}; ImageCache[imgSize][imgName] = imgInfo; if (item.onLoadImg) item.onLoadImg();*/ } } else { if (item.width !== 'undefined' && typeof item.height !== 'undefined') { if (typeof item.angle !== 'undefined') { _this3.ctx.drawImage(item.data, -item.width / 2, -item.height / 2, item.width, item.height); } else { _this3.ctx.drawImage(item.data, item.x, item.y, item.width, item.height); } } else { _this3.ctx.drawImage(item.data, item.x, item.y); } } } if (typeof item.stroke !== 'undefined' && !cStroke) { _this3.ctx.lineWidth = item.stroke.width; _this3.ctx.strokeStyle = item.stroke.color; _this3.ctx.stroke(); } if (typeof item.posDraw !== 'undefined') { item.posDraw(); } if (typeof item.shadow !== 'undefined') { removeShadow(); } if (typeof item.angle !== 'undefined') { _this3.ctx.restore(); } }, points, imgUrl, imgName, imgCopy; for (_iterator.s(); !(_step = _iterator.n()).done;) { _loop(); } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } _this3.width = tWidth > _this3.width ? tWidth : _this3.width; _this3.height = tHeight > _this3.height ? tHeight : _this3.height; }; this.clear = function () { _this3.ctx.clearRect(_this3.x - 2, _this3.y - 2, _this3.width + 4, _this3.height + 4); }; var animation = function animation() { var next = _this3.transform(); _this3.clear(); _this3.update(); draw(); if (!next) _this3.stop(); }; var _animationFrame = function animationFrame() { _this3.clear(); _this3.update(); draw(); if (_this3.transform()) window.requestAnimationFrame(function () { return _animationFrame(); }); }; this.stop = function () { return clearInterval(_this3.interval); }; this.build = function () { if (_this3.onFirstBuild) { _this3.onFirstBuild(); delete _this3.onFirstBuild; } _this3.update(); draw(); if (_this3.onPostBuild) { _this3.onPostBuild(); delete _this3.onPostBuild; } }; this.animate = function (msInterval) { if (_this3.transform) { if (msInterval) _this3.interval = setInterval(animation, msInterval);else window.requestAnimationFrame(function () { return _animationFrame(); }); } else { _this3.build(); } }; } // End Drawable exports.Alignment = Alignment; exports.Drawable = Drawable; exports.Font = Font; exports.FontLoader = FontLoader; exports.Gradient = Gradient; exports.Orientation = Orientation; exports.Shape = Shape; exports.drawable2Image = drawable2Image; })); //# sourceMappingURL=drawable.umd.js.map