/*
ski.js
version 1.9.x
*/
var CORNER, CENTER, CLOSE, SPACE, LEFT, RIGHT, UP, DOWN, SQUARE, ROUND, PROJECT, MITER, BEVEL, DEGREES, RADIANS, PI, TAU, RGBA, HSL, HEX, LEFT_BUTTON, RIGHT_BUTTON, ESCAPE, TAB, SHIFT, CONTROL, ALT, ENTER, BACKSPACE, fps, skiJSData, canvas, ctx, mousePressed, mouseReleased, mouseScrolled, mouseClicked, mouseOver, mouseOut, mouseMoved, mouseIsPressed, mouseButton, mouseX, mouseY, pmouseX, pmouseY, keyPressed, keyReleased, keyTyped, key, keyIsPressed, keyCode, width, height
// constants
// KeyEvent.keyCode values
BACKSPACE = 8
TAB = 9
ENTER = 13
SHIFT = 16
CONTROL = 17
ALT = 18
ESCAPE = 27
SPACE = 32
LEFT = 37
RIGHT = 39
UP = 38
DOWN = 40
//mostly for drawin' stuff on the canvas
CORNER = 0
CENTER = 1
CLOSE = true
SQUARE = atob("YnV0dA==") //yeah, srsly.
ROUND = "round"
PROJECT = "square"
MITER = "iter"
BEVEL = "bevel"
DEGREES = "deg"
RADIANS = "rad"
PI = Math.PI
TAU = PI * 2
RGBA = "rgba"
HSL = "hsl"
HEX = "hex"
LEFT_BUTTON = 0
RIGHT_BUTTON = 2
// setup the canvas
canvas = null
ctx = null
//all canvas-related functions
/**
* sets the canvas width an' height.
* @param {number} w - width
* @param {number} h - height
* @param {boolean} [css=false] - whether to set the dimensions of the canvas or just the resolution
* @example
* size(600, 600)
* //resizes the resolution of the canvas to 600px by 600px
* size(600, 600, true)
* //resizes the resolution an' dimension of the canvas to 600px by 600px
**/
function size (w, h, css) {
if (!canvas || !ctx) {
canvas = document.querySelectorAll("canvas,canvas.skijs,canvas[data-skijs]")[0]
set(canvas)
}
width = canvas.width = w
height = canvas.height = h
css && (canvas.style.width = `${w}px`, canvas.style.height = `${h}px`)
}
/**
* sets the current canvas to a different canvas, whether off or on screen
*
* xxx HS16 - Now attaches the proper event listeners to the canvas.
*
* @param {...(HTMLCanvasElement|OffscreenCanvas|number)} [args] - argument for the set function
* @example
* set()
* //sets the canvas to a new OffscreenCanvas with the same width an' height of the current canvas
* set(document.getElementsByTagName("canvas")[0])
* //set the canvas to the first canvas element in the DOM
* set(400, 400)
* //sets the canvas to a new OffscreenCanvas with dimensions 400px by 400px
**/
function set (...args) {
switch (args.length) {
case 0:
rejectCanvas()
canvas = new OffscreenCanvas(width, height)
ctx = canvas.getContext("2d")
adoptCanvas();
return [canvas, ctx]
break
case 1:
rejectCanvas()
canvas = args[0]
ctx = canvas.getContext("2d")
width = canvas.width
height = canvas.height
adoptCanvas();
break
case 2:
rejectCanvas()
canvas = new OffscreenCanvas(args[0], args[1])
ctx = canvas.getContext("2d")
width = args[0]
height = args[1]
adoptCanvas()
return [canvas, ctx]
}
}
/**
* sets the background for the canvas
* @param {...(string|number|Array)} args
* @see {@link color} for the definition of the arguments
**/
function background (...args) {
const cache = [ctx.strokeStyle, ctx.fillStyle]
const matrix = ctx.getTransform()
ctx.setTransform(1, 0, 0, 1, 0, 0)
ctx.strokeStyle = "rgba(0, 0, 0, 0)"
ctx.fillStyle = color(...args)
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.strokeStyle = cache[0]
ctx.fillStyle = cache[1]
ctx.setTransform(matrix)
}
/**
* sets the fill color for the shapes following the call
* @param {...(string|number|Array)} args
* @see {@link color} for the definition of the arguments
**/
function fill (...args) {
ctx.fillStyle = color(...args)
}
/**
* sets the fill color for the shapes following the call
* @param {...(string|number|Array)} args
* @see {@link color} for the definition of the arguments
**/
function stroke (...args) {
ctx.strokeStyle = color(...args)
}
/**
* draws an ImageData object, HTMLCanvasElement, or HTMLImageElement on the canvas
* @param {(HTMLCanvasElement|ImageData|HTMLImageElement)} img image to be drawn
* @param {number} x
* @param {number} y
* @param {number} [w=img.width] - the width of the image. defaults to the width of the image. i mean what else would it default too lol?
* @param {number} [h=img.height] - the height of the image. defaults to the height of the image. i mean what else would it default too lol?
* @example
* image(img, 0, 0)
* //draws the image at (0, 0) with the default width an' height
* image(img, 0, 0)
* //draws the image at (0, 0) with the dimensions 400px by 400px
**/
function image (img, x, y, w = img.width, h = img.height) {
[x, y] = skiJSData.pos("image", x, y, w, h)
if(img instanceof ImageData) ctx.putImageData(img, x, y, w, h)
else ctx.drawImage(img, x, y, w, h)
}
/**
* draws a clear rectangle across the entire canvas
**/
function clear () {
const matrix = ctx.getTransform()
ctx.setTransform(1, 0, 0, 1, 0, 0)
ctx.clearRect(0, 0, width, height)
ctx.setTransform(matrix)
}
/**
* removes any stroke from the shapes to be drawn after the function call
**/
function noStroke () {
ctx.strokeStyle = color(0, 0)
}
/**
* removes any stroke from the shapes to be drawn after the function call
**/
function noFill () {
ctx.fillStyle = color(0, 0)
}
/**
* draws a rectangle on the canvas
* @param {number} x
* @param {number} y
* @param {number} width
* @param {number} height
* @param {...number} radius - radius or radii. basically works the same as [`ctx.roundRect`]{@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/roundRect}
* @example
* rect(0, 0, 10, 10)
* //draws a rectangle at (0, 0) with dimensions 10px by 10px
**/
function rect (x, y, width, height, ...radius) {
[x, y] = skiJSData.pos("rect", x, y, width, height)
if (radius.length > 0) {
ctx.beginPath()
ctx.roundRect(x, y, width, height, ...radius)
ctx.closePath()
ctx.stroke()
ctx.fill()
} else {
ctx.fillRect(x, y, width, height)
ctx.strokeRect(x, y, width, height)
}
}
/**
* i see absolutely no reason why you would document this, .-.
* it's a helper function for the [text function]{@link text}.
*
* @param {string} str - a string
* @param {number} width - the width of the text
*
* @returns {string} - the string with proper line breaks.
**/
function wrapText (str, width) {
str = str.split(/[ ]/)
let i = 0, phrase = "", result = ""
while(i < str.length){
const frag = str[i]
const newLineCharIndex = frag.indexOf("\n")
if(newLineCharIndex >= 0){
phrase += frag.slice(0, newLineCharIndex + 1)
result += phrase
phrase = frag.slice(newLineCharIndex + 1, frag.length) + " "
}
else {
phrase += frag + " "
if(textWidth(phrase) > width){
result += phrase + "\n"
phrase = ""
}
}
i++
}
if(phrase.length > 0){
result += "\n" + phrase
}
return result
}
/**
* displays text on the canvas
* @param {!*} msg the message you want to display can be anythin' but null.
* @param {number} x
* @param {number} y
* @param {number} [w] width
* @param {number} [h] height - (this doesn't do anythin')
* @example
* textAlign(CENTER, CENTER)
* text("hello there!", width / 2, height / 2)
* //draws "hello there!" on the center of the canvas
* text("hello there!", width / 2, height / 2, 200, 100)
* //draws "hello there!" on the center of the canvas with a maximum width of 200px
**/
function text (msg, x, y, w, h) {
msg = msg?.toString()
if(w) msg = wrapText(msg, w)
if (msg.includes("\n")) {
msg.split("\n").map((p, i) => {
ctx.fillText(p, x, y + ((i - ((msg.split("\n")).length - 1) / 2) * (skiJSData.height + skiJSData.leading)))
ctx.strokeText(p, x, y + ((i - ((msg.split("\n")).length - 1) / 2) * (skiJSData.height + skiJSData.leading)))
})
} else {
ctx.fillText(msg, x, y)
ctx.strokeText(msg, x, y)
}
}
/**
* sets the positionin' mode for rectangles
* @param {number} [m=CORNER] - mode; please only use CORNER an' CENTER or 0 an' 1
* @example
* rectMode(CORNER)
* //draws rectangles from the top-left corner
* rectMode(CENTER)
* //draws rectangles from their center
**/
function rectMode (m) {
skiJSData.rect = m
}
/**
* sets the positionin' mode for ellipses
* @param {number} [m=CENTER] - mode; please only use CORNER an' CENTER or 0 an' 1
* @example
* ellipseMode(CORNER)
* //draws ellipses from the top-left corner
* ellipseMode(CENTER)
* //draws ellipses from their center
**/
function ellipseMode (m) {
skiJSData.ellipse = m
}
/**
* sets the positionin' mode for arcs
* @param {number} [m=CENTER] - mode; please only use CORNER an' CENTER or 0 an' 1
* @example
* arcMode(CORNER)
* //draws arcs from the top-left corner
* arcMode(CENTER)
* //draws arcs from their center
**/
function arcMode (m) {
skiJSData.arc = m
}
/**
* sets the positionin' mode for images
* @param {number} [m=CORNER] - mode; please only use CORNER an' CENTER or 0 an' 1
* @example
* imageMode(CORNER)
* //draws rectangles from the top-left corner
* imageMode(CENTER)
* //draws rectangles from their center
**/
function imageMode (m) {
skiJSData.image = m
}
/**
* aligns the text drawn on the canvas
* @param {number} [horiztonal=CORNER] - horizontal alignment
* @param {number} [vertical=CORNER] - vertical alignment
* @example
* textAlign(CENTER, CENTER)
* //text will now be drawn from the center
**/
function textAlign (horizontal, vertical) {
//println(horizontal === CENTER, vertical === CENTER)
ctx.textAlign = horizontal === CENTER ? "center" : "start"
ctx.textBaseline = vertical === CENTER ? "middle" : "hanging"
}
/**
* do NOT use this function! this is simply for PJS compatibility.
* it's useless, just returns whatever you input as an argument.
* @param {string} font
* @return {string} font
**/
function createFont (font) {
return font
}
/**
* sets the font size for any text drawn
* @param {number} size
* @example
* textSize(20)
* //text will be drawn at 20px
**/
function textSize (size) {
skiJSData.height = size
ctx.font = skiJSData.fontString(skiJSData.font, size)
}
/**
* sets the font for any text drawn
* @param {string} font - the name of the font
* @param {number} [size] - the size of the font in px
* @example
* textFont("Arial", 12)
* //font is arial with size 12px
**/
function textFont (font, size = skiJSData.height) {
skiJSData.height !== size && (skiJSData.height = size)
skiJSData.flags = []
if ((/bold/i).test(font)) {
skiJSData.flags.push("bold")
font = font.replace("bold", '')
}
if ((/italic/i).test(font)) {
skiJSData.flags.push("italic")
font = font.replace("italic", '')
}
font = font.trim()
skiJSData.font = font
ctx.font = skiJSData.fontString(font, size)
}
/**
* sets the space between text on the y-axis
* @param {number} val - leading in pix
**/
function textLeading (val) {
skiJSData.leading = val
}
/**
* find the width of a string given the current font an' font size
* for strings with line breaks, it finds the largest width
* @param {string} txt - text
**/
function textWidth (txt) {
return txt.split("\n").reduce((a, b) => max(a, ctx.measureText(b).width), 0)
}
/**
* i honestly don't know. just ported the concept from PJS.
**/
function textAscent () {
return ctx.measureText("a").fontBoundingBoxAscent
}
/**
* i honestly don't know. just ported the concept from PJS.
**/
function textDescent () {
ctx.measureText("a").fontBoundingBoxDescent
}
/**
* sets the stroke cap. see [`ctx.lineCap`]{@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap}
* @param {string} mode - line cap value; use ski.js constants
**/
function strokeCap (mode) {
ctx.lineCap = mode
}
/**
* sets the stroke cap. see [`ctx.lineJoin`]{@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin}
* @param {string} mode - line join value; use ski.js constants
**/
function strokeJoin (mode) {
ctx.lineJoin = mode
}
/**
* sets the stroke cap. see [`ctx.lineWidth`]{@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineWidth}
* @param {string} weight - line width value; use ski.js constants
**/
function strokeWeight (weight) {
ctx.lineWidth = weight
}
/**
* saves the current state of the canvas
**/
function pushStyle () {
ctx.save()
}
/**
* restores the saved state of the canvas
**/
function popStyle () {
ctx.restore()
}
/**
* save the current transformation matrix state
**/
function pushMatrix () {
const { matrices: arr, matrixToArray: convert } = skiJSData
arr.push(convert(ctx.getTransform()))
}
/**
* restores the saved transformation matrix state
**/
function popMatrix () {
const arr = new Float32Array(skiJSData.matrices.pop())
const mat = DOMMatrix.fromFloat32Array(arr)
ctx.setTransform(mat)
}
/**
* translate given coordinates
* @param {number} x
* @param {number} y
**/
function translate (x, y) {
ctx.transform(1, 0, 0, 1, x, y)
}
/**
* rotate given an angle
* @param {number} ang - rotation angle
**/
function rotate (ang) {
if(skiJSData.angle === DEGREES) ang = degrees(ang)
const cos = Math.cos, sin = Math.sin
ctx.transform(cos(ang), sin(ang), -sin(ang), cos(ang), 0, 0)
}
/**
* scale given a factor
* @param {number} w - if only one arg, the scale factor; else the x scale factor.
* @param {number} [h] - y scale factor
**/
function scale (w, h) {
ctx.transform(w, 0, 0, h ? h : w, 0, 0)
}
/**
* starts a new shape
**/
function beginShape () {
skiJSData.path = []
}
/**
* adds a vertex to a shape
* @param {number} x
* @param {number} y
**/
function vertex (x, y) {
skiJSData.path.push([x, y])
}
/**
* adds a curve vertex to a shape
* a quadratic curve, a singular control point an' an endpoint.
* @param {number} cx - control x
* @param {number} cy - control y
* @param {number} x
* @param {number} y
**/
function curveVertex (cx, cy, x, y) {
skiJSData.path.push([cx, cy, x, y])
}
/**
* adds a bezier vertex to a shape
* a bezier curve, two control points an' one endpoint
* @param {number} c1x - control x 1
* @param {number} c1y - control y 1
* @param {number} c2x - control x 2
* @param {number} c2y - control y 2
* @param {number} x
* @param {number} y
**/
function bezierVertex (c1x, c1y, c2x, c2y, x, y) {
skiJSData.path.push([c1x, c1y, c2x, c2y, x, y])
}
/**
* ends the shape calls an' draws a shape.
* this could be abused to draw the same shape again
* 'cuz i don't clear the path, semi-intentionally.
*
* @param {boolean} [end] - use CLOSE, closes a shape by callin' ctx.closePath
**/
function endShape (end) {
const paths = skiJSData.path
if (paths.length < 2 || paths[0].length !== 2) return
ctx.beginPath()
paths.forEach((path, index) => {
if(index === 0){
ctx.moveTo(...path)
}
else {
if(path.length === 2) ctx.lineTo(...path)
else if(path.length === 4) ctx.quadraticCurveTo(...path)
else if(path.length === 6) ctx.bezierCurveTo(...path)
else return
}
})
if(end) ctx.closePath()
ctx.fill()
ctx.stroke()
}
/**
* draws a curve given six points.
* called in this order: vertex -> curveVertex
* @param {number} sx - start x
* @param {number} sy - start y
* @param {number} cx - control x
* @param {number} cy - control y
* @param {number} ex - end x
* @param {number} ey - end y
**/
function curve (sx, sy, cx, cy, ex, ey) {
if (typeof ey !== "number") return
beginShape()
vertex(sx, sy)
curveVertex(cx, cy, ex, ey)
endShape()
}
/**
* draws a curve given eight points.
* called in this order: vertex -> bezierVertex
* @param {number} sx - start x
* @param {number} sy - start y
* @param {number} c1x - control x 1
* @param {number} c1y - control y 1
* @param {number} c2x - contorl x 2
* @param {number} c2y - control y 2
* @param {number} ex - end x
* @param {number} ey - end y
**/
function bezier (sx, sy, c1x, c1y, c2x, c2y, ex, ey) {
if (typeof ey !== "number") return
beginShape()
vertex(sx, sy)
bezierVertex(c1x, c1y, c2x, c2y, ex, ey)
endShape()
}
/**
* draws an arc.
* @param {number} x
* @param {number} y
* @param {number} w - width
* @param {number} h - width
* @param {number} start - start angle
* @param {number} stop - stop angle
* @param {boolean} [close=false] - call endShape(CLOSE)
**/
function arc (x, y, w, h, start, stop, close = false) {
[x, y] = skiJSData.pos("arc", x, y, width, height)
pushMatrix()
translate(x, y)
if(w !== h){
if(w > h) scale(max(w, h) / min(w, h), 1)
else scale(1, max(w, h) / min(w, h))
}
ctx.beginPath()
if(ctx.fillStyle !== color(0, 0)) ctx.moveTo(0, 0)
ctx.arc(0, 0, min(w, h) / 2, degrees(start), degrees(stop))
if(close) ctx.closePath()
popMatrix()
ctx.fill()
ctx.stroke()
}
/**
* draws an ellipse or circle
* @param {number} x
* @param {number} y
* @param {number} w - width
* @param {number} h - height
**/
function ellipse (x, y, w, h) {
ctx.beginPath()
ctx.ellipse(x, y, w / 2, h / 2, 0, 0, TAU)
ctx.fill()
ctx.stroke()
}
/**
* draws a quadrilateral usin' vertex
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @param {number} x3
* @param {number} y3
* @param {number} x4
* @param {number} y4
**/
function quad (x1, y1, x2, y2, x3, y3, x4, y4) {
beginShape()
vertex(x1, y1)
vertex(x2, y2)
vertex(x3, y3)
vertex(x4, y4)
endShape(CLOSE)
}
/**
* draws a triangle
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @param {number} x3
* @param {number} y3
**/
function triangle (x1, y1, x2, y2, x3, y3) {
beginShape()
vertex(x1, y1)
vertex(x2, y2)
vertex(x3, y3)
endShape(CLOSE)
}
/**
* draws a point with stroke
* @param {number} x
* @param {number} y
**/
function point (x, y) {
if (ctx.strokeStyle !== color(0, 0)) {
const cache = [ctx.strokeStyle, ctx.fillStyle]
noStroke()
ctx.fillStyle = cache[0]
ellipse(x, y, ctx.lineWidth, ctx.lineWidth)
ctx.strokeStyle = cache[0]
ctx.fillStyle = cache[1]
}
}
/**
* draws a line
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
**/
function line (x1, y1, x2, y2) {
ctx.beginPath()
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
ctx.closePath()
ctx.stroke()
}
/**
* returns the ImageData for the given canvas, coordinates an' dimensions
* @param {...(number|HTMLCanvasElement|OffscreenCanvas)} args - [x, y, width, height, sourceCanvas]
* @returns {(ImageData|string)} - the color or ImageData object
* @example
* get()
* //same as get(0, 0, width, height)
* get(0, 0)
* //returns the color of the pixel at (0, 0)
* get(canvas, 0, 0)
* //returns the color of the pixel at (0, 0) on canvas
* get(0, 0, 20, 20)
* //returns the ImageData object for (0, 0) with dimensions 20px by 20px
* get(canvas, 0, 0, 20, 20)
* //returns the ImageData object for (0, 0) with dimensions 20px by 20px on canvas
**/
function get (...args) {
const [x, y, w, h, src] = args
switch (args.length) {
case 0:
return get(0, 0, width, height)
break
case 2:
data = ctx.getImageData(x, y, 1, 1).data
return color(data[0], data[1], data[2], data[3])
break
case 3: {
const canvas = new OffscreenCanvas(w.width, w.height)
const ctx = canvas.getContext("2d")
if (w instanceof HTMLImageElement) {
ctx.drawImage(w, 0, 0)
data = ctx.getImageData(x, y, 1, 1)
} else {
data = w.getContext("2d").getImageData(x, y, 1, 1).data
}
return color(data[0], data[1], data[2], data[3])
}
break
case 4:
const imageCanvas = new OffscreenCanvas(w, h)
const context = imageCanvas.getContext("2d")
context.putImageData(ctx.getImageData(x, y, w, h), 0, 0)
return imageCanvas
break
case 5: {
const canvas = new OffscreenCanvas(src.width, src.height)
const ctx = canvas.getContext("2d")
ctx.drawImage(src, -x, -y)
return canvas
}
}
}
/**
* masks the previous graphics over the followin' graphics
* @example
* set(width, height)
* clear()
* //masking shape
* mask()
* //shapes to be masked
* const img = get(0, 0, width, height)
* set(document.getElementsByTagName("canvas")[0])
* image(img, 0, 0)
**/
function mask () {
ctx.globalCompositeOperation = "source-atop"
}
// event handlers
mouseReleased = () => {}
mouseScrolled = () => {}
mouseClicked = () => {}
mouseOut = () => {}
mouseOver = () => {}
mouseMoved = () => {}
keyPressed = () => {}
keyReleased = () => {}
keyTyped = () => {}
mouseIsPressed = false
mouseButton = LEFT_BUTTON
mouseX = 0
mouseY = 0
pmouseX = mouseX
pmouseY = mouseY
/**
* Attaches event handlers to the current canvas
*/
function adoptCanvas() {
// xxx HS16 - Bind handlers ONLY to the canvas, so a ski.js
// program can be hosted on a website that needs scrolling.
canvas.tabIndex = -1
canvas.onmousedown = e => {
mousePressed(e)
mouseIsPressed = true
mouseButton = e.button
}
canvas.onmousemove = e => {
const rect = canvas.getBoundingClientRect()
pmouseX = mouseX
pmouseY = mouseY
mouseX = constrain(e.pageX - rect.x, 0, width)
mouseY = constrain(e.pageY - rect.y, 0, height)
mouseMoved(e)
}
canvas.onmouseup = e => {
mouseReleased(e)
mouseClicked(e)
mouseButton = e.button
mouseIsPressed = false
e.preventDefault()
}
canvas.oncontextmenu = e => e.preventDefault()
canvas.onmouseover = e => mouseOver(e)
canvas.onmouseout = e => mouseOut(e)
canvas.onwheel = e => {
mouseScrolled(e)
e.preventDefault()
}
canvas.onkeydown = e => {
e.preventDefault()
key = e.key
keyCode = e.keyCode
keyIsPressed = true
keyPressed(e)
}
canvas.onkeyup = e => {
e.preventDefault()
key = e.key
keyCode = e.keyCode
keyReleased(e)
}
canvas.onkeypress = e => {
e.preventDefault()
key = e.key
keyCode = e.keyCode
keyTyped(e)
}
}
/**
* Removes all event listeners from a canvas, in anticipation
* of a new host.
*/
function rejectCanvas() {
if (!canvas) return;
canvas.onmousedown = null
canvas.onmousemove = null
canvas.onmouseup = null
canvas.oncontextmenu = null
canvas.onmouseover = null
canvas.onmouseout = null
canvas.onwheel = null
canvas.onkeydown = null
canvas.onkeyup = null
canvas.onkeypress = null
}
// data used by ski.js
skiJSData = {
//canvas-related
rect: CORNER,
ellipse: CENTER,
arc: CENTER,
image: CORNER,
leading: 0,
height: 12,
flags: [],
fontString(font, size) {
let flags = ""
if(this.flags.includes("bold")) flags += "bold "
if(this.flags.includes("italics")) flags += "italics "
return (flags + `${size}px ` + font)
},
pos(type, x, y, w, h) {
return type === "rect" || type === "image" ? this[type] < 1 ? [x, y] : [x - w / 2, y - h / 2] : this[type] < 1 ? [x + w / 2, y + w / 2] : [x, y]
},
matrixToArray(matrix) {
return ["a", "b", "c", "d", "e", "f"].map(el => matrix[el])
},
matrices: [],
path: [],
//non-canvas related
rate: 60,
millis: 0,
start: 0,
draw: 0,
angle: DEGREES,
color: RGBA,
}
// FPS
fps = 60
// miscellaneous
/**
* alias for console.debug
* @param {...*} args
*
**/
function debug (...args) {
console.debug(...args)
}
/**
* alias for console.assert
* @param {...*} args
**/
function isEqual (...args) {
console.assert(...args)
}
/**
* returns the day
* @returns {number} - day
**/
function day () {
return (new Date).getDate()
}
/**
* returns the month
* @returns {number} - month
**/
function month () {
return (new Date).getMonth()
}
/**
* returns the year
* @returns {number} - year
**/
function year () {
return (new Date).getYear()
}
/**
* returns the hours
* @returns {number} - hours
**/
function hour () {
return (new Date).getHours()
}
/**
* returns the minutes
* @returns {number} - minutes
**/
function minute () {
return (new Date).getMinutes()
}
/**
* returns the seconds
* @returns {number} - seconds
**/
function seconds () {
return (new Date).getSeconds()
}
/**
* enables the context menu, so you can right-click an' save the canvas as an image.
**/
function enableContextMenu () {
canvas.oncontextmenu = true
}
/**
* alias for document.body.style.cursor
* @param {string} name - name of the cursor
**/
function cursor (name) {
document.body.style.cursor = name
}
/**
* enables image smoothing
**/
function smooth () {
ctx.imageSmoothingEnabled = true
ctx.imageSmoothingQuality = "high"
}
/**
* disables image smoothing
**/
function noSmooth () {
ctx.imageSmoothingEnabled = false
ctx.imageSmoothingQuality = "low"
}
/**
* disables the draw function
**/
function noLoop () {
skiJSData.draw = draw
draw = 0
}
/**
* enables the draw function
**/
function loop () {
draw = skiJSData.draw || draw
}
// math
/**
* returns the maximum value from two values
* does not find the maximum of all values for efficiency's sake
* if such a function is necessary, this will do in a pinch:
* const maxAll = (...args) => args.reduce((a, b) => max(a, b), 0)
* @param {number} a
* @param {number} b
* @return {number} - the maximum
**/
function max (a, b) {
return a > b ? a : b
}
/**
* returns the minimum value from two values
* does not find the minimum of all values for efficiency's sake
* if such a function is necessary, this will do in a pinch:
* const minAll = (...args) => args.reduce((a, b) => min(a, b), 0)
* @param {number} a
* @param {number} b
* @return {number} - the minimum
**/
function min (a, b) {
return a < b ? a : b
}
/**
* returns the magnitude of two values
* @param {number} a
* @param {number} b
* @returns {number} - the magnitude
**/
function mag (a, b) {
return Math.sqrt((a ** 2) + (b ** 2))
}
/**
* returns the euclidean distance between two coordinates
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @returns {number} - the euclidean distance
**/
function dist (x1, y1, x2, y2) {
return mag(x2 - x1, y2 - y1)
}
/**
* returns the manhattan distance between two coordinates
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @returns {number} - the euclidean distance
**/
function manhattanDistance (x1, y1, x2, y2) {
return abs(x1 - x2) + abs(y1 - y2)
}
/**
* returns the chebyshev distance between two coordinates
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @returns {number} - the euclidean distance
**/
function chebyshevDistance (x1, y1, x2, y2) {
return max(abs(x1 - x2), abs(y1 - y2))
}
/**
* take the natural number to a number n
* @param {number} n - power to raise the natural number to
* @returns {number}
**/
function exp (n) {
return Math.E ** n
}
/**
* normalizes a value
* @param {number} val - value to normalize
* @param {number} low - lowest value
* @param {number} high - highest value
* @returns {number} - the normalized value
**/
function norm (val, low, high) {
return (val - low) / (high - low)
}
/**
* map a value to two ranges
* @param {number} val - value to normalize
* @param {number} a - lowest value of range 1
* @param {number} b - highest value of range 1
* @param {number} c - lowest value of range 2
* @param {number} d - highest value of range 2
* @returns {number} - the mapped value
**/
function map (val, a, b, c, d) {
return c + (d - c) * norm(val, a, b)
}
/**
* linearly interpolates a value
* @param {number} val - value to interpolate
* @param {number} targ - value to interpolate to
* @param {number} amt - amount to interpolate by
* @returns {number} - interpolated value
**/
function lerp (val, targ, amt) {
return ((targ - val) * amt) + val
}
/**
* returns a random value usin' Math.random
* @param {number} [min] - if max, minimum value generated; else, maximum value generated with the minimum value being 0
* @param {number} [max] - maximum value generated
* @returns {number} - random value
**/
function random (min, max) {
if(max) return Math.random() * (max - min) + min
else if(min) return Math.random() * min
else return Math.random()
}
/**
* constrain a value between two values
* @param {number} val - value to be constrained
* @param {number} low - lowest value that the value can be
* @param {number} high - highest value that the value can be
* @returns {number} - constrained value
**/
function constrain (val, low, high) {
return min(max(val, low), high)
}
/**
* returns the logarithm of a number
* alias for Math.log
* @param {number} n
* @returns {number}
**/
function log (n){
return Math.log(n)
}
/**
* returns the square root of a number
* alias for Math.sqrt
* @param {number} n
* @returns {number}
**/
function sqrt (n) {
return Math.sqrt(n)
}
/**
* returns the square of a number
* alias for n ** 2
* @param {number} n
* @returns {number}
**/
function sq (n) {
return n ** 2
}
/**
* returns the result of a base an' a power
* alias for a ** b
* @param {number} a - base
* @param {number} b - power
* @returns {number}
**/
function pow (a, b) {
return a ** b
}
/**
* returns the absolute value of a number
* @param {number} n
* @returns {number}
**/
function abs (n) {
return n < 0 ? -n : n
}
/**
* returns the truncated number
* @param {number} n
* @returns {number}
**/
function trunc (n) {
return n | 0
}
/**
* returns the floored number
* @param {number} n
* @returns {number}
**/
function floor (n) {
return n < 0 ? (n | 0) - 1 : n | 0
}
/**
* returns the ceilinged number
* @param {number} n
* @returns {number}
**/
function ceil (n) {
return floor(n) + 1
}
/**
* returns the rounded number
* @param {number} n
* @returns {number}
**/
function round (n) {
return n - floor(n) < 0.5 ? floor(n) : ceil(n)
}
/** TODO **/
/**
* returns the sine of an angle
* alias for Math.sin
* @param {number} ang - angle
* @returns {number}
**/
function sin (ang) {
if(skiJSData.angle === DEGREES) ang = degrees(ang)
return Math.sin(ang)
}
/**
* returns the cosine of an angle
* alias for Math.cos
* @param {number} ang - angle
* @returns {number}
**/
function cos (ang) {
if(skiJSData.angle === DEGREES) ang = degrees(ang)
return Math.cos(ang)
}
/**
* returns the tangent of an angle
* alias for Math.tan
* @param {number} ang - angle
* @returns {number}
**/
function tan (ang) {
if(skiJSData.angle === DEGREES) ang = degrees(ang)
return Math.tan(ang)
}
/**
* returns the arccosine of an angle
* alias for Math.acos
* @param {number} ang - angle
* @returns {number}
**/
function acos (ang) {
if(skiJSData.angle === DEGREES) ang = degrees(ang)
return Math.acos(ang)
}
/**
* returns the arcsine of an angle
* alias for Math.asin
* @param {number} ang - angle
* @returns {number}
**/
function asin (ang) {
if(skiJSData.angle === DEGREES) ang = degrees(ang)
return Math.asin(ang)
}
/**
* returns the arctangent of an angle
* alias for Math.atan
* @param {number} ang - angle
* @returns {number}
**/
function atan (ang) {
ang = Math.atan(ang)
if(skiJSData.angle === DEGREES) ang = radians(ang)
return ang
}
/**
* alias for Math.atan2
* @param {number} y
* @param {number} x
* @returns {number}
**/
function atan2 (y, x) {
let ang = Math.atan2(y, x)
if(skiJSData.angle === DEGREES) ang = radians(ang)
return ang
}
/**
* returns the radian value of an angle in degrees
* @param {number} ang - angle
* @returns {number}
**/
function radians (ang) {
return ang * (180 / PI)
}
/**
* returns the degrees of an angle in radians
* @param {number} ang - angle
* @returns {number}
**/
function degrees (ang) {
return ang * (PI / 180)
}
/**
* sets the angle mode
* @param {string} mode - use DEGREES or RADIANS
**/
function angleMode (mode) {
skiJSData.angle = mode
}
/**
* evaluates the bezier at a point given t
* @param {number} a - control x 1
* @param {number} b - control y 1
* @param {number} c - control x 2
* @param {number} d - control y 2
* @param {number} t - value between 0 an' 1
* @returns {number}
**/
function bezierPoint (a, b, c, d, t) {
return (1 - t) * (1 - t) * (1 - t) * a + 3 * (1 - t) * (1 - t) * t * b + 3 * (1 - t) * t * t * c + t * t * t * d
}
/**
* evaluates the bezier at a tangent given t
* @param {number} a - control x 1
* @param {number} b - control y 1
* @param {number} c - control x 2
* @param {number} d - control y 2
* @param {number} t - value between 0 an' 1
* @returns {number}
**/
function bezierTangent (a, b, c, d, t) {
return (3 * t * t * (-a + 3 * b - 3 * c + d) + 6 * t * (a - 2 * b + c) + 3 * (-a + b))
}
/**
* sets the color mode
* @param {string} mode - use RGBA or HEX or HSL
**/
function colorMode (mode) {
return skiJSData.color = mode
}
/**
* returns the color given a variety of arguments.
* this is a monster function that handles a lot of cases.
* @params {...*} args - see examples below
* @example
* color(25)
* color([25])
* //returns "rgba(25, 25, 25, 1)"
* color(25, 100)
* color([25, 100])
* //returns "rgba(25, 25, 25, 0.3921...)" where 0.3921... = 100 / 255
* color(175, 250, 175)
* color([175, 250, 175])
* //returns "rgba(175, 250, 175, 1)"
* color(175, 250, 175, 100)
* color([175, 250, 175, 100])
* //returns "rgba(175, 250, 175, 0.3921...)" where 0.3921... = 100 / 255
* colorMode(HSL)
* color(147, 50, 47)
* color([147, 50, 47])
* color("hsl(147, 50%, 47%)")
* //returns "hsl(147, 50%, 47%)"
* colorMode(HEX)
* color("#000")
* //returns "#000"
* color("rgba(25, 25, 25, 1)")
* //returns "rgba(25, 25, 25, 1)"
**/
function color (...args) {
if (typeof args[0] === "string" && args.length <= 1 && (/(#|rgb|hsl|rgba)/).test(args[0])) return args[0]
args[0] instanceof Array && (args = args[0])
if (typeof args[1] === "number" && (/rgb|rgba/).test(args[0])) {
let cache = args[0].match(/[0-9\.]+(?=(,|\)))/g)
args = [cache[0], cache[1], cache[2], args[1]]
}
switch (skiJSData.color) {
case RGBA:
const [r, g, b, a] = args.length > 4 ? Object.assign(args, {
length: 4
}) : args
switch (args.length) {
case 1:
return `rgba(${r}, ${r}, ${r}, 1)`
break
case 2:
return `rgba(${r}, ${r}, ${r}, ${g / 255})`
break
case 3:
return `rgba(${r}, ${g}, ${b}, 1)`
break
case 4:
return `rgba(${r}, ${g}, ${b}, ${a / 255})`
}
break
case HSL:
return `hsl(${args[0]}, ${args[1]}%, ${args[2]}%)`
break
case HEX:
return args[0]
}
}
/**
* uses `lerp` to linerally interpolate two rgba color values
* @param {string} color1 - use `color`
* @param {string} color2 - use `color`
* @param {number} amt - amount to lerp
* @returns {string} - the lerped color
**/
function lerpColor (color1, color2, amt) {
if (typeof color1 !== "string" || typeof color2 !== "string" || skiJSData.color !== RGBA)
return
const [r1, g1, b1, a1] = color1.match(/\d{1,3}/g)
const [r2, g2, b2, a2] = color2.match(/\d{1,3}/g)
return `rgba(${lerp(+r1, +r2, amt)}, ${lerp(+g1, +g2, amt)}, ${lerp(+b1, +b2, amt)}, ${lerp(+a1, +a2, amt)})`
}
/**
* returns the red value of the color
* @param {...(string|number|Array)} args
* @returns {number} - the red value
* @see {@link color} for the definition of the arguments
**/
function red (...args) {
if(skiJSData.color !== RGBA) return
const col = color(...args)
return +col.match(/\d+(\.|)\d*/)
}
/**
* returns the green value of the color
* @param {...(string|number|Array)} args
* @returns {number} - the green value
* @see {@link color} for the definition of the arguments
**/
function green (...args) {
if(skiJSData.color !== RGBA) return
const col = color(...args)
return +col.match(/\d+(\.|)\d*/g)[1]
}
/**
* returns the blue value of the color
* @param {...(string|number|Array)} args
* @returns {number} - the blue value
* @see {@link color} for the definition of the arguments
**/
function blue (...args) {
if(skiJSData.color !== RGBA) return
const col = color(...args)
return +col.match(/\d+(\.|)\d*/g)[2]
}
/**
* returns the alpha value of the color
* @param {...(string|number|Array)} args
* @returns {number} - the alpha value
* @see {@link color} for the definition of the arguments
**/
function alpha (...args) {
if(skiJSData.color !== RGBA) return
const col = color(...args)
return trunc(col.match(/\d+(\.|)\d*/g)[3] * 255)
}
/**
* returns a promise that resolves once an image has been fetched.
* @params {string} src - image source
* @params {number} [width] - width of the image
* @params {number} [height] - height of the image
* @returns {Promise}
**/
function getImage (src, width, height) {
return new Promise((resolve, reject) => {
let img = loadImage(src, width, height)
//resolve or reject
img.onload = () => resolve(img)
img.onerror = () => reject("invalid or unaccessible image source")
})
}
/**
* returns an image
* @params {string} src - image source
* @params {number} [width] - width of the image
* @params {number} [height] - height of the image
* @returns {Image}
**/
function loadImage (src, width, height) {
let img
//dimensions
if(width) img = new Image(width, height)
else img = new Image
//source
img.src = src
if(!(/khanacademy/).test(src)) img.crossOrigin = "anonymous"
return img
}
/**
* returns a promise that resolves once all fonts has been fetched.
* @params {...string} fonts - all fonts
* @returns {Promise}
* @example
* getFont("Roboto", "Comfortaa")
* //fetches Roboto an' Comfortaa from Google Fonts
**/
function getFont (...fonts) {
return new Promise ((res, rej) => {
// xxx HS16 - Efficiency :P
const link = loadFont(...fonts)
link.onload = () => res(link)
link.onerror = () => reject("invalid or unaccessible fonts. not rlly, it's just an error lol. idk what went wrong, but you're router or DNS is blockin' fonts.googleapis.com")
})
}
/**
* returns an HTMLLinkElement that loads the fonts from Google Fonts
* @params {...string} fonts - all fonts
* @returns {HTMLLinkElement}
* @example
* loadFont("Roboto", "Comfortaa")
* //fetches Roboto an' Comfortaa from Google Fonts
**/
function loadFont (...fonts) {
const link = document.createElement("link")
link.rel = "stylesheet"
link.href = `https://fonts.googleapis.com/css?family=${fonts.join("|").replace(/ /g, "+")}`
document.body.appendChild(link)
return link
}
// animation
/**
* sets the frame rate
* @params {number} rate - the frame rate in fps; note that 60 is the max due to JS restrictions
**/
function frameRate (rate) {
skiJSData.rate = rate
}
/**
* returns the milliseconds that have elapsed since the program started.
* @returns {number}
**/
function millis () {
return skiJSData.millis
}
frameCount = 0
delta = 1000 / 60
then = performance.now()
skiJSData.start = performance.now()
function raf (time) {
requestAnimationFrame(raf)
delta = time - then
let ms = 1000 / skiJSData.rate
if (delta < ms) return
let overflow = delta % ms
then = time - overflow
delta -= overflow
draw_standin(time)
frameCount += 1
skiJSData.millis = performance.now() - skiJSData.start
fps = 1000 / delta
}
/**
* due to the way the KA environment is set up, the `draw` function works a lil' funny.
* this is all PT's work (shoutout to him). you came to the docs, so here's a quick
* explanation of how it works in case you run into any problems.
* first, a new property of `window` is defined, `draw`.
* second, we attach it to a function called `draw_standin`
* when you set `draw`, the `draw_standin` function is set an' `raf` is run.
* there is an argument that you can call from draw, time which works much like
* performance.now().
**/
Object.defineProperty(window, "draw", {
get() {
return draw_standin
},
set(func) {
typeof draw_standin !== "function" && requestAnimationFrame(raf)
draw_standin = func
},
configurable: true
})
//whew.