1
0
mirror of https://gitee.com/coder-xiaomo/algorithm-visualization synced 2025-09-09 22:21:39 +08:00
Code Issues Projects Releases Wiki Activity GitHub Gitee
Files
algorithm-visualization/src/assets/js/class.js

639 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* WorkSpace class v0.1.0
*
* @author coder-xiaomo
* @date 2022-05-15
*
* Released under the MIT license
*/
class WorkSpace {
static settings = null;
static primaryCanvas = null;
constructor(settings) {
this.settings = settings
settings.workSpace = this
// 清除原有内容
settings.container
.style("width", settings.width)
.style("height", settings.height)
// .attr("width", settings.width)
// .attr("height", settings.height)
.html("")
// 创建工作区SVG
this.primaryCanvas = settings.container.append("svg")
// 添加id
.attr("id", "primaryCanvas")
// 设置 SVG 宽高
.attr("width", "100%")
.attr("height", "100%")
// 背景色
.style("background-color", settings.colorMap["background"])
}
}
class ViBase {
static workSpace = null;
constructor(workSpace) {
this.workSpace = workSpace
}
}
class ArrayVi extends ViBase {
// [47, 11, 50, 13, 16, 49, 8, 9, 38, 27, 20]
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
static listData
// 网页加载完毕初始化事件
initialize({ elementId }) {
let controlDiv = document.getElementById("control-div")
controlDiv.style.textAlign = "center"
var sortClassList = getSortClassList();
console.log(sortClassList);
var DOMFragment = document.createDocumentFragment()
var selector = document.createElement("select")
for (let i = 0; i < sortClassList.length; i++) {
const sortClass = new sortClassList[i](animation)
const sortClassInfo = sortClass.info()
// 跳过未完成的算法
if (!sortClassInfo['available'])
continue
let option = document.createElement("option")
option.value = sortClassInfo['name']
option.innerText = sortClassInfo['name']
selector.appendChild(option)
}
DOMFragment.appendChild(selector)
let ctrlBtn = document.createElement("button")
ctrlBtn.innerHTML = "开始排序"
let that = this
ctrlBtn.onclick = function () {
// 点击排序算法按钮
// if (!that.updateListDataArray(elementId, { doNotAlert: false }))
// return
if (!that.listData || that.listData.length == 0) {
alert("数组为空")
return
}
// 隐藏一些东西,显示一些东西
controlDiv.style.display = 'none'
d3.select("#console-div")
.style("display", "")
d3.select("#console-current-algorithm")
.style("text-align", "center")
.html(selector.value/*sortClassInfo['name']*/)
// 找到对应的算法,然后开始排序
for (let i = 0; i < sortClassList.length; i++) {
const sortClass = new sortClassList[i](animation)
const sortClassInfo = sortClass.info()
if (sortClassInfo['name'] === selector.value) {
sortClass.doSortWithAnimation(elementId)
break
}
}
}
DOMFragment.appendChild(ctrlBtn)
controlDiv.appendChild(DOMFragment)
// 生成一个随机数组
this.randomListDataArray(elementId)
// 显示 siderbar
d3.select("#sidebar").style("display", "")
}
// 将数组显示到输入框中
updateListDataInput() {
document.getElementById("array-input").value = this.listData.join("")
}
// Array 内容改变时
updateListDataArray(elementId, { doNotAlert = false }) {
var val = document.getElementById("array-input").value
try {
var preList = val.replaceAll("", ",").split(",")
this.listData = []
preList.forEach(element => {
if (element.trim() === "" || isNaN(element))
return
this.listData.push(Number(element))
});
arrayVi.initArray(elementId, this.listData)
} catch (err) {
console.log(err)
if (!doNotAlert)
alert("输入不正确,请检查!")
return false
}
return true
}
// 随机 Array
randomListDataArray(elementId) {
function getRandom(length) {
return Math.floor(Math.random() * length); // 可均衡获取 0 到 length-1 的随机整数。
}
// 获取一个 6 - 12 以内的随机数
var len = 6 + getRandom(13 - 6)
this.listData = []
for (let i = 0; i < len; i++) {
this.listData.push(getRandom(51))
}
this.updateListDataInput()
this.updateListDataArray(elementId, { doNotAlert: false })
}
// 绘制数组
initArray(elementId, listData) {
console.log("initArray")
let fragment = shape.getLinkedListFragment(elementId, listData, {
x: 100,
y: 100,
width: "100px",
height: "100px",
})
// console.log(fragment)
workSpace.primaryCanvas.html("")
// 添加水印 居中
var watermarkWidth = settings.outerSize.height * 0.65
shape.addWatermark(elementId, {
imageSrc: "./assets/image/logo-small.svg",
})
.attr('id', 'watermark-c-c')
.attr('x', settings.outerSize.width / 2)
.attr('y', settings.outerSize.height / 2)
.style('width', watermarkWidth + 'px')
.style('height', watermarkWidth + 'px')
.style('transform', `translate(-${watermarkWidth / 2}px, -${watermarkWidth / 2}px)`)
.style('opacity', '0.015')
.style('transition', '0.2s')
// 添加水印 右下角
var watermarkWidth = 60
shape.addWatermark(elementId, {
imageSrc: "./assets/image/logo-small.svg",
})
.attr('id', 'watermark-r-b')
.attr('x', settings.outerSize.width)
.attr('y', settings.outerSize.height)
.style('width', watermarkWidth + 'px')
.style('height', watermarkWidth + 'px')
.style('transform', 'translate(-80px, -80px)')
workSpace.primaryCanvas.node().appendChild(fragment)
document.getElementById(elementId).customAttr = fragment.customAttr
}
}
/**
* Shape class v0.1.0
*
* @author coder-xiaomo
* @date 2022-05-15
*
* Released under the MIT license
*/
class Shape {
static workSpace = null;
constructor(workSpace) {
this.workSpace = workSpace
}
addShape(shape, id) {
var settings = this.workSpace.settings
return workSpace.primaryCanvas.append(shape)
.style("transform", `translate(${settings.margin.left}px, ${settings.margin.top}px)`)
.attr("id", id)
}
addShape_NoTransform(shape, id) {
return workSpace.primaryCanvas.append(shape)
.attr("id", id)
}
// 添加矩形
rectangle(id, { x, y, width, height, fillColor = "white", strokeColor = "black" }) {
return this.addShape("rect", id)
.attr("x", x)
.attr("y", y)
.attr("width", width)
.attr("height", height)
.style("fill", fillColor)
.style("stroke", strokeColor)
}
// 添加圆形
circle(id, { x, y, radius, fillColor = "white", strokeColor = "black" }) {
return this.addShape("circle", id)
.attr("cx", x)
.attr("cy", y)
.attr("r", radius)
.style("fill", fillColor)
.style("stroke", strokeColor)
}
// 添加文本
text(id, { x, y, text, fillColor }) {
return this.addShape("text", id)
.attr("x", x)
.attr("y", y)
.text(text)
.style("fill", fillColor)
}
// 添加线
line(id, { x1, y1, x2, y2, strokeColor }) {
return this.addShape("line", id)
.attr("x1", x1)
.attr("y1", y1)
.attr("x2", x2)
.attr("y2", y2)
.style("stroke", strokeColor)
}
// 添加路径
path(id, d, { fillColor = "white", strokeColor = "black" }) {
return this.addShape("path", id)
.attr("d", d)
.style("fill", fillColor)
.style("stroke", strokeColor)
}
// 添加坐标轴
axis(id, { transform, axis }) {
return this.addShape_NoTransform("g", id)
.attr("transform", transform)
.call(axis)
}
// 添加一个链表节点
addNode(id, x, y, width, height, text) {
var primaryCanvas = workSpace.primaryCanvas
primaryCanvas.append("rect", id)
.attr("x", x)
.attr("y", y)
.attr("width", width)
.attr("height", height)
.style("fill", workSpace.settings.colorMap["fill"])
.style("stroke", workSpace.settings.colorMap["stroke"])
primaryCanvas.append("text", id + "_text")
.attr("x", x)
.attr("y", y)
// .attr("x", x + width / 2)
// .attr("y", y + height / 2)
.text(text)
.style("fill", workSpace.settings.colorMap["text"])
}
// 添加一个图片水印
addWatermark(id, { imageSrc }) {
var primaryCanvas = workSpace.primaryCanvas
return primaryCanvas.append("image", id)
.attr("xlink:href", imageSrc)
}
// 绘制一个链表
getLinkedListFragment(id, nodes) {
var settings = this.workSpace.settings
let displayMaxWidth = 1 * settings.innerSize.width
let displayMaxHeight = 1 * settings.innerSize.height
let areaWidth = 1 * nodes.length // 按照1个Unit来计算
let areaHeight = 1 // 按照1个Unit来计算
let oneUnit = 100
// 可以假设高度相等比较宽度,这样好理解
if (displayMaxWidth / displayMaxHeight > areaWidth / areaHeight) {
// 展示区域左右有多余空间
oneUnit = displayMaxHeight / areaHeight
} else {
// 展示区域上下有空间(或刚刚好)
oneUnit = displayMaxWidth / areaWidth
}
// 定义最大值
if (oneUnit > 120) oneUnit = 120
let fragment = document.createDocumentFragment()
fragment.customAttr = {
id: id,
nodes: nodes,
type: "linkedList",
oneUnit: oneUnit,
gsapTimeline: gsap.timeline({
onStart: function () {
consoleClear()
},
onComplete: function () {
consoleLog(`排序完成`)
console.log("all done")
// this.seek(0)
}
})
}
// console.log(fragment.customAttr)
// <g></g> 元素不能设置 width 和 height
let g = d3.select(fragment)
.append("svg:g")
.attr("id", id)
.attr("fill", "white")
.attr("transform", `translate(${settings.margin.left}, ${settings.margin.top})`)
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i]
let _g = g.append("svg:g")
_g.append("svg:rect")
.attr("x", i * oneUnit)
.attr("y", 0)
.attr("width", oneUnit)
.attr("height", oneUnit)
.attr("fill", settings.colorMap["fill"])
.attr("stroke", settings.colorMap["stroke"])
_g.append("svg:text")
.text(node)
.attr("x", i * oneUnit + oneUnit / 2)
.attr("y", oneUnit / 2)
.attr("width", oneUnit)
.attr("height", oneUnit)
.attr("fill", settings.colorMap["text"])
// 调试用
if (settings.debugMode)
_g.append("svg:text")
.text(i)
.attr("x", i * oneUnit)
.attr("y", oneUnit)
.attr("width", oneUnit)
.attr("height", oneUnit)
.attr("fill", "black")
// console.log(text.node().getBBox())
// console.log(text.node().getBoundingClientRect())
}
// g.append("svg:rect")
// .attr("width", oneUnit * nodes.length)
// .attr("height", oneUnit)
// .style("fill", "none")
// .style("stroke", "green")
return fragment
}
}
class VectorAnimation {
constructor(workSpace) {
this.workSpace = workSpace
}
swapElementAttr(attrName/* or attrNameList */, element1, element2) {
function exchange(attrName) {
// 保存 element1 的属性,将 element2 的属性赋值给 element1, 再将保存的属性赋值给 element2
var tmp = element1.getAttribute(attrName)
element1.setAttribute(attrName, element2.getAttribute(attrName))
element2.setAttribute(attrName, tmp)
}
if (typeof attrName === "string") {
exchange(attrName)
} else if (typeof attrName === "object") {
for (let i = 0; i < attrName.length; i++) {
exchange(attrName[i])
}
}
}
swapElementInnerHTML(element1, element2) {
var tmp = element1.innerHTML
element1.innerHTML = element2.innerHTML
element2.innerHTML = tmp
}
swapElementsAttr(elementPairList) {
// [
// [attrName or attrNameList, element1, element2],
// [attrName or attrNameList, element1, element2],
// ...
// ]
for (let i = 0; i < elementPairList.length; i++) {
let elementPair = elementPairList[i]
this.swapElementAttr(elementPair[0], elementPair[1], elementPair[2])
}
}
// 交换数组元素
swapLinkedListItems(id, [fromIndex, toIndex]) {
if (fromIndex < toIndex)
[fromIndex, toIndex] = [toIndex, fromIndex]
var settings = this.workSpace.settings
let linkedList = document.getElementById(id)
let customAttr = linkedList.customAttr
// console.log(customAttr)
var gList = linkedList.childNodes
let from = gList[fromIndex]
let to = gList[toIndex]
var deltaX = customAttr.oneUnit * (fromIndex - toIndex);
var deltaY = customAttr.oneUnit * 1.08
var animateSettings = this.workSpace.settings.animation.getConf()
// 如果是相邻的两个元素交换,或者要交换的两个元素是同一个元素
if (Math.abs(fromIndex - toIndex) <= 1) {
deltaY /= 2
animateSettings.duration *= 0.6
}
var that = this
let timeline = gsap.timeline({
onStart: function () {
displayCurrentArray(customAttr.nodes)
consoleLog(`交换索引为 ${fromIndex}${toIndex} 的两个元素`)
},
onComplete: function () {
// 交换DOM元素中的值
that.swapElementInnerHTML(from.childNodes[1], to.childNodes[1])
// 交换 node 列表中的值
// console.log(customAttr.nodes)
let tmp = customAttr.nodes[fromIndex]
customAttr.nodes[fromIndex] = customAttr.nodes[toIndex]
customAttr.nodes[toIndex] = tmp
// console.log(customAttr.nodes)
displayCurrentArray(customAttr.nodes)
console.log("animation done (swap)")
}
}).add([
gsap.to(from.childNodes[0], { ...animateSettings, fill: settings.colorMap["fill_focus"] }),
gsap.to(to.childNodes[0], { ...animateSettings, fill: settings.colorMap["fill_focus"] }),
gsap.to(from.childNodes[1], { ...animateSettings, fill: settings.colorMap["text_focus"] }),
gsap.to(to.childNodes[1], { ...animateSettings, fill: settings.colorMap["text_focus"] }),
gsap.to(from.childNodes, { ...animateSettings, y: -deltaY }),
gsap.to(to.childNodes, { ...animateSettings, y: deltaY }),
]).add([
gsap.to(from.childNodes, { ...animateSettings, x: -deltaX }),
gsap.to(to.childNodes, { ...animateSettings, x: deltaX }),
]).add([
gsap.to(from.childNodes[0], { ...animateSettings, fill: settings.colorMap["fill"] }),
gsap.to(to.childNodes[0], { ...animateSettings, fill: settings.colorMap["fill"] }),
gsap.to(from.childNodes[1], { ...animateSettings, fill: settings.colorMap["text"] }),
gsap.to(to.childNodes[1], { ...animateSettings, fill: settings.colorMap["text"] }),
gsap.to(from.childNodes, { ...animateSettings, y: 0 }),
gsap.to(to.childNodes, { ...animateSettings, y: 0 }),
]).add([
// 恢复到动画之前的状态
gsap.to(from.childNodes, { ...animateSettings, duration: 0, x: 0, y: 0 }),
gsap.to(to.childNodes, { ...animateSettings, duration: 0, x: 0, y: 0 }),
])
customAttr.gsapTimeline.add(timeline)
}
// 高亮数组元素
highlightLinkedListItems(id, indexList, onStartCallback) {
let linkedList = document.getElementById(id)
let customAttr = linkedList.customAttr
var gList = linkedList.childNodes
if (typeof (indexList) === "number")
indexList = [indexList]
var hightlightElementList_fill = []
var hightlightElementList_text = []
for (let i = 0; i < indexList.length; i++) {
const index = indexList[i];
hightlightElementList_fill.push(gList[index].childNodes[0])
hightlightElementList_text.push(gList[index].childNodes[1])
}
var animateSettings = this.workSpace.settings.animation.getConf()
let timeline = gsap.timeline({
onStart: function () {
displayCurrentArray(customAttr.nodes)
if (onStartCallback)
onStartCallback()
},
onComplete: function () {
console.log("animation done (hightlight)")
}
}).add([
gsap.to(hightlightElementList_fill, { ...animateSettings, fill: settings.colorMap["fill_focus"] }),
gsap.to(hightlightElementList_text, { ...animateSettings, fill: settings.colorMap["text_focus"] }),
]).add([
gsap.to(hightlightElementList_fill, { ...animateSettings, fill: settings.colorMap["fill"] }),
gsap.to(hightlightElementList_text, { ...animateSettings, fill: settings.colorMap["text"] }),
])
customAttr.gsapTimeline.add(timeline)
}
// 比较数组元素
compareLinkedListItems(id, index1, index2) {
this.highlightLinkedListItems(id, [index1, index2], function () {
consoleLog(`比较索引为 ${index1}${index2} 的两个元素`)
})
}
// 弹出/弹回 数组元素
popupLinkedListItems(id, index, { popBack = false }) {
let linkedList = document.getElementById(id)
let customAttr = linkedList.customAttr
var gList = linkedList.childNodes
var deltaY = customAttr.oneUnit * 1.08
if (popBack) deltaY = 0
let timeline = gsap.timeline({
onStart: function () {
if (typeof (onStartCallback) === "function")
onStartCallback()
},
onComplete: function () {
console.log("animation done (popup)")
}
}).add([
gsap.to(gList[index], { ...this.workSpace.settings.animation.getConf(), y: deltaY }),
])
customAttr.gsapTimeline.add(timeline)
}
// 交换相邻数组元素位置(仅在水平方向呼唤,垂直方向不做调整) 【fromIndex 是y方向上突出的元素】
exchangeLinkedListItems(id, fromIndex, toIndex) {
if (fromIndex < toIndex)
[fromIndex, toIndex] = [toIndex, fromIndex]
var settings = this.workSpace.settings
let linkedList = document.getElementById(id)
let customAttr = linkedList.customAttr
// console.log(customAttr)
var gList = linkedList.childNodes
let from = gList[fromIndex]
let to = gList[toIndex]
var deltaX = customAttr.oneUnit * (fromIndex - toIndex);
var deltaY = customAttr.oneUnit * 1.08 // 要跟 popupLinkedListItems 函数中的 deltaY 完全一致
var animateSettings = settings.animation.getConf()
var that = this
let timeline = gsap.timeline({
onStart: function () {
displayCurrentArray(customAttr.nodes)
consoleLog(`交换索引为 ${fromIndex}${toIndex} 的两个元素`)
},
onComplete: function () {
// 交换DOM元素中的值
that.swapElementInnerHTML(from.childNodes[1], to.childNodes[1])
// 交换 node 列表中的值
// console.log(customAttr.nodes)
let tmp = customAttr.nodes[fromIndex]
customAttr.nodes[fromIndex] = customAttr.nodes[toIndex]
customAttr.nodes[toIndex] = tmp
// console.log(customAttr.nodes)
displayCurrentArray(customAttr.nodes)
console.log("animation done (exchange)")
}
}).add([
gsap.to(from.childNodes, { ...animateSettings, x: -deltaX }),
gsap.to(to.childNodes, { ...animateSettings, x: deltaX }),
]).add([
// 恢复到动画之前的状态
gsap.to(from.childNodes, { ...animateSettings, duration: 0, x: 0 }),
gsap.to(to.childNodes, { ...animateSettings, duration: 0, x: 0 }),
// 由于两个元素一个突出一个不突出所以还要交换两个y
gsap.to(from, { ...animateSettings, duration: 0, y: 0 }),
gsap.to(to, { ...animateSettings, duration: 0, y: deltaY }),
])
customAttr.gsapTimeline.add(timeline)
}
}