transform tool
This commit is contained in:
parent
c66f84c1ed
commit
016e8148ed
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
219
src/main.js
219
src/main.js
|
|
@ -113,6 +113,7 @@ let context = {
|
|||
selectionRect: undefined,
|
||||
selection: [],
|
||||
shapeselection: [],
|
||||
dragDirection: undefined,
|
||||
}
|
||||
|
||||
let config = {
|
||||
|
|
@ -316,8 +317,6 @@ let actions = {
|
|||
},
|
||||
execute: (action) => {
|
||||
let frame = pointerList[action.frame]
|
||||
console.log(pointerList)
|
||||
console.log(action.frame)
|
||||
frame.keys = structuredClone(action.newState)
|
||||
},
|
||||
rollback: (action) => {
|
||||
|
|
@ -368,7 +367,6 @@ let actions = {
|
|||
} else if (layer.frames[frameNum].frameType != "keyframe") {
|
||||
formerType = layer.frames[frameNum].frameType
|
||||
} else {
|
||||
console.log("foolish")
|
||||
return // Already a keyframe, nothing to do
|
||||
}
|
||||
redoStack.length = 0
|
||||
|
|
@ -1200,7 +1198,8 @@ class GraphicsObject {
|
|||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.rotation = 0; // in radians
|
||||
this.scale = 1;
|
||||
this.scale_x = 1;
|
||||
this.scale_y = 1;
|
||||
if (!uuid) {
|
||||
this.idx = uuidv4()
|
||||
} else {
|
||||
|
|
@ -1234,7 +1233,6 @@ class GraphicsObject {
|
|||
} else if (this.activeLayer.frames[num].frameType == "motion") {
|
||||
let frameKeys = {}
|
||||
const t = (num - this.activeLayer.frames[num].prevIndex) / (this.activeLayer.frames[num].nextIndex - this.activeLayer.frames[num].prevIndex);
|
||||
console.log(this.activeLayer.frames[num].prev)
|
||||
for (let key in this.activeLayer.frames[num].prev.keys) {
|
||||
frameKeys[key] = {}
|
||||
let prevKeyDict = this.activeLayer.frames[num].prev.keys[key]
|
||||
|
|
@ -1287,17 +1285,19 @@ class GraphicsObject {
|
|||
growBoundingBox(bbox, child.bbox())
|
||||
}
|
||||
}
|
||||
bbox.x.max *= this.scale_x
|
||||
bbox.y.max *= this.scale_y
|
||||
bbox.x.min += this.x
|
||||
bbox.x.max += this.x
|
||||
bbox.y.min += this.y
|
||||
bbox.y.max += this.y
|
||||
console.log(bbox)
|
||||
return bbox
|
||||
}
|
||||
draw(context) {
|
||||
let ctx = context.ctx;
|
||||
ctx.translate(this.x, this.y)
|
||||
ctx.rotate(this.rotation)
|
||||
ctx.scale(this.scale_x, this.scale_y)
|
||||
// if (this.currentFrameNum>=this.maxFrame) {
|
||||
// this.currentFrameNum = 0;
|
||||
// }
|
||||
|
|
@ -1316,7 +1316,8 @@ class GraphicsObject {
|
|||
child.x = this.currentFrame.keys[idx].x;
|
||||
child.y = this.currentFrame.keys[idx].y;
|
||||
child.rotation = this.currentFrame.keys[idx].rotation;
|
||||
child.scale = this.currentFrame.keys[idx].scale;
|
||||
child.scale_x = this.currentFrame.keys[idx].scale_x;
|
||||
child.scale_y = this.currentFrame.keys[idx].scale_y;
|
||||
ctx.save()
|
||||
child.draw(context)
|
||||
if (true) {
|
||||
|
|
@ -1363,28 +1364,66 @@ class GraphicsObject {
|
|||
ctx.fill()
|
||||
ctx.restore()
|
||||
}
|
||||
for (let item of context.selection) {
|
||||
ctx.save()
|
||||
ctx.strokeStyle = "#00ffff"
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath()
|
||||
let bbox = item.bbox()
|
||||
ctx.rect(bbox.x.min, bbox.y.min, bbox.x.max - bbox.x.min, bbox.y.max - bbox.y.min)
|
||||
ctx.stroke()
|
||||
ctx.restore()
|
||||
}
|
||||
if (context.selectionRect) {
|
||||
ctx.save()
|
||||
ctx.strokeStyle = "#00ffff"
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath()
|
||||
ctx.rect(
|
||||
context.selectionRect.x1, context.selectionRect.y1,
|
||||
context.selectionRect.x2 - context.selectionRect.x1,
|
||||
context.selectionRect.y2 - context.selectionRect.y1
|
||||
)
|
||||
ctx.stroke()
|
||||
ctx.restore()
|
||||
if (mode == "select") {
|
||||
for (let item of context.selection) {
|
||||
ctx.save()
|
||||
ctx.strokeStyle = "#00ffff"
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath()
|
||||
let bbox = item.bbox()
|
||||
ctx.rect(bbox.x.min, bbox.y.min, bbox.x.max - bbox.x.min, bbox.y.max - bbox.y.min)
|
||||
ctx.stroke()
|
||||
ctx.restore()
|
||||
}
|
||||
if (context.selectionRect) {
|
||||
ctx.save()
|
||||
ctx.strokeStyle = "#00ffff"
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath()
|
||||
ctx.rect(
|
||||
context.selectionRect.x1, context.selectionRect.y1,
|
||||
context.selectionRect.x2 - context.selectionRect.x1,
|
||||
context.selectionRect.y2 - context.selectionRect.y1
|
||||
)
|
||||
ctx.stroke()
|
||||
ctx.restore()
|
||||
}
|
||||
} else if (mode == "transform") {
|
||||
let bbox = undefined;
|
||||
for (let item of context.selection) {
|
||||
if (bbox==undefined) {
|
||||
bbox = structuredClone(item.bbox())
|
||||
} else {
|
||||
growBoundingBox(bbox, item.bbox())
|
||||
}
|
||||
}
|
||||
if (bbox != undefined) {
|
||||
// ctx.save()
|
||||
// ctx.strokeStyle = "#00ffff"
|
||||
// ctx.lineWidth = 1;
|
||||
// ctx.beginPath()
|
||||
// let xdiff = bbox.x.max - bbox.x.min
|
||||
// let ydiff = bbox.y.max - bbox.y.min
|
||||
// ctx.rect(
|
||||
// bbox.x.min, bbox.y.min,
|
||||
// xdiff,
|
||||
// ydiff
|
||||
// )
|
||||
// ctx.stroke()
|
||||
// ctx.fillStyle = "#000000"
|
||||
// let rectRadius = 5
|
||||
// for (let i of [[0,0],[0.5,0],[1,0],[1,0.5],[1,1],[0.5,1],[0,1],[0,0.5]]) {
|
||||
// ctx.beginPath()
|
||||
// ctx.rect(
|
||||
// bbox.x.min + xdiff * i[0] - rectRadius,
|
||||
// bbox.y.min + ydiff * i[1] - rectRadius,
|
||||
// rectRadius*2, rectRadius*2
|
||||
// )
|
||||
// ctx.fill()
|
||||
// }
|
||||
|
||||
// ctx.restore()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1398,7 +1437,8 @@ class GraphicsObject {
|
|||
x: x,
|
||||
y: y,
|
||||
rotation: 0,
|
||||
scale: 1,
|
||||
scale_x: 1,
|
||||
scale_y: 1,
|
||||
}
|
||||
}
|
||||
removeShape(shape) {
|
||||
|
|
@ -1659,7 +1699,6 @@ function addFrame() {
|
|||
}
|
||||
|
||||
function addKeyframe() {
|
||||
console.log(context.activeObject.currentFrameNum)
|
||||
actions.addKeyframe.create()
|
||||
}
|
||||
|
||||
|
|
@ -1737,10 +1776,56 @@ async function render() {
|
|||
function stage() {
|
||||
let stage = document.createElement("canvas")
|
||||
let scroller = document.createElement("div")
|
||||
let stageWrapper = document.createElement("div")
|
||||
stage.className = "stage"
|
||||
stage.width = 1500
|
||||
stage.height = 1000
|
||||
scroller.className = "scroll"
|
||||
stageWrapper.className = "stageWrapper"
|
||||
let selectionRect = document.createElement("div")
|
||||
selectionRect.className = "selectionRect"
|
||||
for (let i of ["nw", "n", "ne", "e", "se", "s", "sw", "w"]) {
|
||||
let cornerRect = document.createElement("div")
|
||||
cornerRect.classList.add("cornerRect")
|
||||
cornerRect.classList.add(i)
|
||||
cornerRect.addEventListener('mousedown', (e) => {
|
||||
console.log(i)
|
||||
let bbox = undefined;
|
||||
let selection = {}
|
||||
for (let item of context.selection) {
|
||||
if (bbox==undefined) {
|
||||
bbox = structuredClone(item.bbox())
|
||||
} else {
|
||||
growBoundingBox(bbox, item.bbox())
|
||||
}
|
||||
selection[item.idx] = {x: item.x, y: item.y, scale_x: item.scale_x, scale_y: item.scale_y}
|
||||
}
|
||||
if (bbox != undefined) {
|
||||
context.dragDirection = i
|
||||
context.activeTransform = {
|
||||
initial: {
|
||||
x: {min: bbox.x.min, max: bbox.x.max},
|
||||
y: {min: bbox.y.min, max: bbox.y.max},
|
||||
selection: selection
|
||||
},
|
||||
current: {
|
||||
x: {min: bbox.x.min, max: bbox.x.max},
|
||||
y: {min: bbox.y.min, max: bbox.y.max},
|
||||
selection: structuredClone(selection)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
cornerRect.addEventListener('mouseup', (e) => {
|
||||
const newEvent = new MouseEvent(e.type, e);
|
||||
stage.dispatchEvent(newEvent)
|
||||
})
|
||||
cornerRect.addEventListener('mousemove', (e) => {
|
||||
const newEvent = new MouseEvent(e.type, e);
|
||||
stage.dispatchEvent(newEvent)
|
||||
})
|
||||
selectionRect.appendChild(cornerRect)
|
||||
}
|
||||
stage.addEventListener("drop", (e) => {
|
||||
e.preventDefault()
|
||||
let mouse = getMousePos(stage, e)
|
||||
|
|
@ -1784,7 +1869,9 @@ function stage() {
|
|||
e.preventDefault()
|
||||
})
|
||||
canvases.push(stage)
|
||||
scroller.appendChild(stage)
|
||||
stageWrapper.appendChild(stage)
|
||||
stageWrapper.appendChild(selectionRect)
|
||||
scroller.appendChild(stageWrapper)
|
||||
stage.addEventListener("mousedown", (e) => {
|
||||
let mouse = getMousePos(stage, e)
|
||||
switch (mode) {
|
||||
|
|
@ -1884,6 +1971,7 @@ function stage() {
|
|||
stage.addEventListener("mouseup", (e) => {
|
||||
context.mouseDown = false
|
||||
context.dragging = false
|
||||
context.dragDirection = undefined
|
||||
context.selectionRect = undefined
|
||||
let mouse = getMousePos(stage, e)
|
||||
switch (mode) {
|
||||
|
|
@ -2038,6 +2126,40 @@ function stage() {
|
|||
}
|
||||
context.lastMouse = mouse
|
||||
break;
|
||||
case "transform":
|
||||
if (context.dragDirection) {
|
||||
let initial = context.activeTransform.initial
|
||||
let current = context.activeTransform.current
|
||||
let initialSelection = context.activeTransform.initial.selection
|
||||
if (context.dragDirection.indexOf('n') != -1) {
|
||||
current.y.min = mouse.y
|
||||
} else if (context.dragDirection.indexOf('s') != -1) {
|
||||
current.y.max = mouse.y
|
||||
}
|
||||
if (context.dragDirection.indexOf('w') != -1) {
|
||||
current.x.min = mouse.x
|
||||
} else if (context.dragDirection.indexOf('e') != -1) {
|
||||
current.x.max = mouse.x
|
||||
}
|
||||
// Calculate the translation difference between current and initial values
|
||||
const delta_x = current.x.min - initial.x.min;
|
||||
const delta_y = current.y.min - initial.y.min;
|
||||
|
||||
// Calculate the scaling factor based on the difference between current and initial values
|
||||
const scale_x_ratio = (current.x.max - current.x.min) / (initial.x.max - initial.x.min);
|
||||
const scale_y_ratio = (current.y.max - current.y.min) / (initial.y.max - initial.y.min);
|
||||
|
||||
for (let idx in initialSelection) {
|
||||
let item = context.activeObject.currentFrame.keys[idx]
|
||||
let xoffset = initialSelection[idx].x - initial.x.min
|
||||
let yoffset = initialSelection[idx].y - initial.y.min
|
||||
item.x = initial.x.min + delta_x + xoffset * scale_x_ratio
|
||||
item.y = initial.y.min + delta_y + yoffset * scale_y_ratio
|
||||
item.scale_x = initialSelection[idx].scale_x * scale_x_ratio
|
||||
item.scale_y = initialSelection[idx].scale_y * scale_y_ratio
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
@ -2060,6 +2182,7 @@ function toolbar() {
|
|||
toolbtn.addEventListener("click", () => {
|
||||
mode = tool
|
||||
updateInfopanel()
|
||||
updateUI()
|
||||
console.log(tool)
|
||||
})
|
||||
}
|
||||
|
|
@ -2267,7 +2390,6 @@ function splitPane(div, percent, horiz, newPane=undefined) {
|
|||
const frac = getMousePositionFraction(event, event.currentTarget)
|
||||
div.setAttribute("lb-percent", frac*100)
|
||||
updateAll()
|
||||
console.log(frac); // Ensure the fraction is between 0 and 1
|
||||
}
|
||||
});
|
||||
div.addEventListener('mouseup', (event) => {
|
||||
|
|
@ -2328,17 +2450,38 @@ function updateUI() {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
for (let selectionRect of document.querySelectorAll(".selectionRect")) {
|
||||
selectionRect.style.display = "none"
|
||||
}
|
||||
if (mode == "transform") {
|
||||
if (context.selection.length > 0) {
|
||||
for (let selectionRect of document.querySelectorAll(".selectionRect")) {
|
||||
let bbox = undefined;
|
||||
for (let item of context.selection) {
|
||||
if (bbox==undefined) {
|
||||
bbox = structuredClone(item.bbox())
|
||||
} else {
|
||||
growBoundingBox(bbox, item.bbox())
|
||||
}
|
||||
}
|
||||
if (bbox != undefined) {
|
||||
selectionRect.style.display = "block"
|
||||
selectionRect.style.left = `${bbox.x.min}px`
|
||||
selectionRect.style.top = `${bbox.y.min}px`
|
||||
selectionRect.style.width = `${bbox.x.max - bbox.x.min}px`
|
||||
selectionRect.style.height = `${bbox.y.max - bbox.y.min}px`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateLayers() {
|
||||
console.log(document.querySelectorAll(".layers-container"))
|
||||
for (let container of document.querySelectorAll(".layers-container")) {
|
||||
let layerspanel = container.querySelectorAll(".layers")[0]
|
||||
let framescontainer = container.querySelectorAll(".frames-container")[0]
|
||||
layerspanel.textContent = ""
|
||||
framescontainer.textContent = ""
|
||||
console.log(context.activeObject)
|
||||
for (let layer of context.activeObject.layers) {
|
||||
let layerHeader = document.createElement("div")
|
||||
layerHeader.className = "layer-header"
|
||||
|
|
@ -2347,11 +2490,9 @@ function updateLayers() {
|
|||
layerTrack.className = "layer-track"
|
||||
framescontainer.appendChild(layerTrack)
|
||||
layerTrack.addEventListener("click", (e) => {
|
||||
console.log(layerTrack.getBoundingClientRect())
|
||||
let mouse = getMousePos(layerTrack, e)
|
||||
let frameNum = parseInt(mouse.x/25)
|
||||
context.activeObject.currentFrameNum = frameNum
|
||||
console.log(context.activeObject )
|
||||
updateLayers()
|
||||
updateMenu()
|
||||
updateUI()
|
||||
|
|
@ -2640,4 +2781,4 @@ const panes = {
|
|||
name: "infopanel",
|
||||
func: infopanel
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ body {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.logo.vanilla:hover {
|
||||
filter: drop-shadow(0 0 2em #ffe21c);
|
||||
}
|
||||
|
|
@ -174,7 +178,85 @@ button {
|
|||
.stage {
|
||||
width: 1500px;
|
||||
height: 1000px;
|
||||
overflow: scroll;
|
||||
/* overflow: scroll; */
|
||||
}
|
||||
.stageWrapper {
|
||||
position: relative;
|
||||
width: 1500px;
|
||||
height: 1500px;
|
||||
}
|
||||
.selectionRect {
|
||||
position: absolute;
|
||||
width: 500px;
|
||||
height: 300px;
|
||||
left: 100px;
|
||||
top: 100px;
|
||||
border: 1px solid #00ffff;
|
||||
display: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
.cornerRect {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: black;
|
||||
transition: width 0.2s ease, height 0.2s linear;
|
||||
user-select: none;
|
||||
|
||||
pointer-events: auto;
|
||||
}
|
||||
.cornerRect:hover {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
.nw {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
transform: translate(-50%, -50%);
|
||||
cursor:nw-resize;
|
||||
}
|
||||
.n {
|
||||
top: 0px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
cursor:n-resize;
|
||||
}
|
||||
.ne {
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
transform: translate(50%, -50%);
|
||||
cursor:ne-resize;
|
||||
}
|
||||
.e {
|
||||
top: 50%;
|
||||
right: 0px;
|
||||
transform: translate(50%, -50%);
|
||||
cursor:e-resize;
|
||||
}
|
||||
.se {
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
transform: translate(50%, 50%);
|
||||
cursor:se-resize;
|
||||
}
|
||||
.s {
|
||||
bottom: 0px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 50%);
|
||||
cursor:s-resize;
|
||||
}
|
||||
.sw {
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
transform: translate(-50%, 50%);
|
||||
cursor:sw-resize;
|
||||
}
|
||||
.w {
|
||||
top: 50%;
|
||||
left: 0px;
|
||||
transform: translate(-50%, -50%);
|
||||
cursor:w-resize;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,573 @@
|
|||
/*
|
||||
var vid = new Whammy.Video();
|
||||
vid.add(canvas or data url)
|
||||
vid.compile()
|
||||
*/
|
||||
|
||||
window.Whammy = (function(){
|
||||
// in this case, frames has a very specific meaning, which will be
|
||||
// detailed once i finish writing the code
|
||||
|
||||
function toWebM(frames, outputAsArray){
|
||||
var info = checkFrames(frames);
|
||||
|
||||
//max duration by cluster in milliseconds
|
||||
var CLUSTER_MAX_DURATION = 30000;
|
||||
|
||||
var EBML = [
|
||||
{
|
||||
"id": 0x1a45dfa3, // EBML
|
||||
"data": [
|
||||
{
|
||||
"data": 1,
|
||||
"id": 0x4286 // EBMLVersion
|
||||
},
|
||||
{
|
||||
"data": 1,
|
||||
"id": 0x42f7 // EBMLReadVersion
|
||||
},
|
||||
{
|
||||
"data": 4,
|
||||
"id": 0x42f2 // EBMLMaxIDLength
|
||||
},
|
||||
{
|
||||
"data": 8,
|
||||
"id": 0x42f3 // EBMLMaxSizeLength
|
||||
},
|
||||
{
|
||||
"data": "webm",
|
||||
"id": 0x4282 // DocType
|
||||
},
|
||||
{
|
||||
"data": 2,
|
||||
"id": 0x4287 // DocTypeVersion
|
||||
},
|
||||
{
|
||||
"data": 2,
|
||||
"id": 0x4285 // DocTypeReadVersion
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 0x18538067, // Segment
|
||||
"data": [
|
||||
{
|
||||
"id": 0x1549a966, // Info
|
||||
"data": [
|
||||
{
|
||||
"data": 1e6, //do things in millisecs (num of nanosecs for duration scale)
|
||||
"id": 0x2ad7b1 // TimecodeScale
|
||||
},
|
||||
{
|
||||
"data": "whammy",
|
||||
"id": 0x4d80 // MuxingApp
|
||||
},
|
||||
{
|
||||
"data": "whammy",
|
||||
"id": 0x5741 // WritingApp
|
||||
},
|
||||
{
|
||||
"data": doubleToString(info.duration),
|
||||
"id": 0x4489 // Duration
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 0x1654ae6b, // Tracks
|
||||
"data": [
|
||||
{
|
||||
"id": 0xae, // TrackEntry
|
||||
"data": [
|
||||
{
|
||||
"data": 1,
|
||||
"id": 0xd7 // TrackNumber
|
||||
},
|
||||
{
|
||||
"data": 1,
|
||||
"id": 0x73c5 // TrackUID
|
||||
},
|
||||
{
|
||||
"data": 0,
|
||||
"id": 0x9c // FlagLacing
|
||||
},
|
||||
{
|
||||
"data": "und",
|
||||
"id": 0x22b59c // Language
|
||||
},
|
||||
{
|
||||
"data": "V_VP8",
|
||||
"id": 0x86 // CodecID
|
||||
},
|
||||
{
|
||||
"data": "VP8",
|
||||
"id": 0x258688 // CodecName
|
||||
},
|
||||
{
|
||||
"data": 1,
|
||||
"id": 0x83 // TrackType
|
||||
},
|
||||
{
|
||||
"id": 0xe0, // Video
|
||||
"data": [
|
||||
{
|
||||
"data": info.width,
|
||||
"id": 0xb0 // PixelWidth
|
||||
},
|
||||
{
|
||||
"data": info.height,
|
||||
"id": 0xba // PixelHeight
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 0x1c53bb6b, // Cues
|
||||
"data": [
|
||||
//cue insertion point
|
||||
]
|
||||
}
|
||||
|
||||
//cluster insertion point
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
var segment = EBML[1];
|
||||
var cues = segment.data[2];
|
||||
|
||||
//Generate clusters (max duration)
|
||||
var frameNumber = 0;
|
||||
var clusterTimecode = 0;
|
||||
while(frameNumber < frames.length){
|
||||
|
||||
var cuePoint = {
|
||||
"id": 0xbb, // CuePoint
|
||||
"data": [
|
||||
{
|
||||
"data": Math.round(clusterTimecode),
|
||||
"id": 0xb3 // CueTime
|
||||
},
|
||||
{
|
||||
"id": 0xb7, // CueTrackPositions
|
||||
"data": [
|
||||
{
|
||||
"data": 1,
|
||||
"id": 0xf7 // CueTrack
|
||||
},
|
||||
{
|
||||
"data": 0, // to be filled in when we know it
|
||||
"size": 8,
|
||||
"id": 0xf1 // CueClusterPosition
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
cues.data.push(cuePoint);
|
||||
|
||||
var clusterFrames = [];
|
||||
var clusterDuration = 0;
|
||||
do {
|
||||
clusterFrames.push(frames[frameNumber]);
|
||||
clusterDuration += frames[frameNumber].duration;
|
||||
frameNumber++;
|
||||
}while(frameNumber < frames.length && clusterDuration < CLUSTER_MAX_DURATION);
|
||||
|
||||
var clusterCounter = 0;
|
||||
var cluster = {
|
||||
"id": 0x1f43b675, // Cluster
|
||||
"data": [
|
||||
{
|
||||
"data": Math.round(clusterTimecode),
|
||||
"id": 0xe7 // Timecode
|
||||
}
|
||||
].concat(clusterFrames.map(function(webp){
|
||||
var block = makeSimpleBlock({
|
||||
discardable: 0,
|
||||
frame: webp.data.slice(4),
|
||||
invisible: 0,
|
||||
keyframe: 1,
|
||||
lacing: 0,
|
||||
trackNum: 1,
|
||||
timecode: Math.round(clusterCounter)
|
||||
});
|
||||
clusterCounter += webp.duration;
|
||||
return {
|
||||
data: block,
|
||||
id: 0xa3
|
||||
};
|
||||
}))
|
||||
}
|
||||
|
||||
//Add cluster to segment
|
||||
segment.data.push(cluster);
|
||||
clusterTimecode += clusterDuration;
|
||||
}
|
||||
|
||||
//First pass to compute cluster positions
|
||||
var position = 0;
|
||||
for(var i = 0; i < segment.data.length; i++){
|
||||
if (i >= 3) {
|
||||
cues.data[i-3].data[1].data[1].data = position;
|
||||
}
|
||||
var data = generateEBML([segment.data[i]], outputAsArray);
|
||||
position += data.size || data.byteLength || data.length;
|
||||
if (i != 2) { // not cues
|
||||
//Save results to avoid having to encode everything twice
|
||||
segment.data[i] = data;
|
||||
}
|
||||
}
|
||||
|
||||
return generateEBML(EBML, outputAsArray)
|
||||
}
|
||||
|
||||
// sums the lengths of all the frames and gets the duration, woo
|
||||
|
||||
function checkFrames(frames){
|
||||
var width = frames[0].width,
|
||||
height = frames[0].height,
|
||||
duration = frames[0].duration;
|
||||
for(var i = 1; i < frames.length; i++){
|
||||
if(frames[i].width != width) throw "Frame " + (i + 1) + " has a different width";
|
||||
if(frames[i].height != height) throw "Frame " + (i + 1) + " has a different height";
|
||||
if(frames[i].duration < 0 || frames[i].duration > 0x7fff) throw "Frame " + (i + 1) + " has a weird duration (must be between 0 and 32767)";
|
||||
duration += frames[i].duration;
|
||||
}
|
||||
return {
|
||||
duration: duration,
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function numToBuffer(num){
|
||||
var parts = [];
|
||||
while(num > 0){
|
||||
parts.push(num & 0xff)
|
||||
num = num >> 8
|
||||
}
|
||||
return new Uint8Array(parts.reverse());
|
||||
}
|
||||
|
||||
function numToFixedBuffer(num, size){
|
||||
var parts = new Uint8Array(size);
|
||||
for(var i = size - 1; i >= 0; i--){
|
||||
parts[i] = num & 0xff;
|
||||
num = num >> 8;
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
function strToBuffer(str){
|
||||
// return new Blob([str]);
|
||||
|
||||
var arr = new Uint8Array(str.length);
|
||||
for(var i = 0; i < str.length; i++){
|
||||
arr[i] = str.charCodeAt(i)
|
||||
}
|
||||
return arr;
|
||||
// this is slower
|
||||
// return new Uint8Array(str.split('').map(function(e){
|
||||
// return e.charCodeAt(0)
|
||||
// }))
|
||||
}
|
||||
|
||||
|
||||
//sorry this is ugly, and sort of hard to understand exactly why this was done
|
||||
// at all really, but the reason is that there's some code below that i dont really
|
||||
// feel like understanding, and this is easier than using my brain.
|
||||
|
||||
function bitsToBuffer(bits){
|
||||
var data = [];
|
||||
var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : '';
|
||||
bits = pad + bits;
|
||||
for(var i = 0; i < bits.length; i+= 8){
|
||||
data.push(parseInt(bits.substr(i,8),2))
|
||||
}
|
||||
return new Uint8Array(data);
|
||||
}
|
||||
|
||||
function generateEBML(json, outputAsArray){
|
||||
var ebml = [];
|
||||
for(var i = 0; i < json.length; i++){
|
||||
if (!('id' in json[i])){
|
||||
//already encoded blob or byteArray
|
||||
ebml.push(json[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
var data = json[i].data;
|
||||
if(typeof data == 'object') data = generateEBML(data, outputAsArray);
|
||||
if(typeof data == 'number') data = ('size' in json[i]) ? numToFixedBuffer(data, json[i].size) : bitsToBuffer(data.toString(2));
|
||||
if(typeof data == 'string') data = strToBuffer(data);
|
||||
|
||||
if(data.length){
|
||||
var z = z;
|
||||
}
|
||||
|
||||
var len = data.size || data.byteLength || data.length;
|
||||
var zeroes = Math.ceil(Math.ceil(Math.log(len)/Math.log(2))/8);
|
||||
var size_str = len.toString(2);
|
||||
var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str;
|
||||
var size = (new Array(zeroes)).join('0') + '1' + padded;
|
||||
|
||||
//i actually dont quite understand what went on up there, so I'm not really
|
||||
//going to fix this, i'm probably just going to write some hacky thing which
|
||||
//converts that string into a buffer-esque thing
|
||||
|
||||
ebml.push(numToBuffer(json[i].id));
|
||||
ebml.push(bitsToBuffer(size));
|
||||
ebml.push(data)
|
||||
|
||||
|
||||
}
|
||||
|
||||
//output as blob or byteArray
|
||||
if(outputAsArray){
|
||||
//convert ebml to an array
|
||||
var buffer = toFlatArray(ebml)
|
||||
return new Uint8Array(buffer);
|
||||
}else{
|
||||
return new Blob(ebml, {type: "video/webm"});
|
||||
}
|
||||
}
|
||||
|
||||
function toFlatArray(arr, outBuffer){
|
||||
if(outBuffer == null){
|
||||
outBuffer = [];
|
||||
}
|
||||
for(var i = 0; i < arr.length; i++){
|
||||
if(typeof arr[i] == 'object'){
|
||||
//an array
|
||||
toFlatArray(arr[i], outBuffer)
|
||||
}else{
|
||||
//a simple element
|
||||
outBuffer.push(arr[i]);
|
||||
}
|
||||
}
|
||||
return outBuffer;
|
||||
}
|
||||
|
||||
//OKAY, so the following two functions are the string-based old stuff, the reason they're
|
||||
//still sort of in here, is that they're actually faster than the new blob stuff because
|
||||
//getAsFile isn't widely implemented, or at least, it doesn't work in chrome, which is the
|
||||
// only browser which supports get as webp
|
||||
|
||||
//Converting between a string of 0010101001's and binary back and forth is probably inefficient
|
||||
//TODO: get rid of this function
|
||||
function toBinStr_old(bits){
|
||||
var data = '';
|
||||
var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : '';
|
||||
bits = pad + bits;
|
||||
for(var i = 0; i < bits.length; i+= 8){
|
||||
data += String.fromCharCode(parseInt(bits.substr(i,8),2))
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function generateEBML_old(json){
|
||||
var ebml = '';
|
||||
for(var i = 0; i < json.length; i++){
|
||||
var data = json[i].data;
|
||||
if(typeof data == 'object') data = generateEBML_old(data);
|
||||
if(typeof data == 'number') data = toBinStr_old(data.toString(2));
|
||||
|
||||
var len = data.length;
|
||||
var zeroes = Math.ceil(Math.ceil(Math.log(len)/Math.log(2))/8);
|
||||
var size_str = len.toString(2);
|
||||
var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str;
|
||||
var size = (new Array(zeroes)).join('0') + '1' + padded;
|
||||
|
||||
ebml += toBinStr_old(json[i].id.toString(2)) + toBinStr_old(size) + data;
|
||||
|
||||
}
|
||||
return ebml;
|
||||
}
|
||||
|
||||
//woot, a function that's actually written for this project!
|
||||
//this parses some json markup and makes it into that binary magic
|
||||
//which can then get shoved into the matroska comtainer (peaceably)
|
||||
|
||||
function makeSimpleBlock(data){
|
||||
var flags = 0;
|
||||
if (data.keyframe) flags |= 128;
|
||||
if (data.invisible) flags |= 8;
|
||||
if (data.lacing) flags |= (data.lacing << 1);
|
||||
if (data.discardable) flags |= 1;
|
||||
if (data.trackNum > 127) {
|
||||
throw "TrackNumber > 127 not supported";
|
||||
}
|
||||
var out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function(e){
|
||||
return String.fromCharCode(e)
|
||||
}).join('') + data.frame;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// here's something else taken verbatim from weppy, awesome rite?
|
||||
|
||||
function parseWebP(riff){
|
||||
var VP8 = riff.RIFF[0].WEBP[0];
|
||||
|
||||
var frame_start = VP8.indexOf('\x9d\x01\x2a'); //A VP8 keyframe starts with the 0x9d012a header
|
||||
for(var i = 0, c = []; i < 4; i++) c[i] = VP8.charCodeAt(frame_start + 3 + i);
|
||||
|
||||
var width, horizontal_scale, height, vertical_scale, tmp;
|
||||
|
||||
//the code below is literally copied verbatim from the bitstream spec
|
||||
tmp = (c[1] << 8) | c[0];
|
||||
width = tmp & 0x3FFF;
|
||||
horizontal_scale = tmp >> 14;
|
||||
tmp = (c[3] << 8) | c[2];
|
||||
height = tmp & 0x3FFF;
|
||||
vertical_scale = tmp >> 14;
|
||||
return {
|
||||
width: width,
|
||||
height: height,
|
||||
data: VP8,
|
||||
riff: riff
|
||||
}
|
||||
}
|
||||
|
||||
// i think i'm going off on a riff by pretending this is some known
|
||||
// idiom which i'm making a casual and brilliant pun about, but since
|
||||
// i can't find anything on google which conforms to this idiomatic
|
||||
// usage, I'm assuming this is just a consequence of some psychotic
|
||||
// break which makes me make up puns. well, enough riff-raff (aha a
|
||||
// rescue of sorts), this function was ripped wholesale from weppy
|
||||
|
||||
function parseRIFF(string){
|
||||
var offset = 0;
|
||||
var chunks = {};
|
||||
|
||||
while (offset < string.length) {
|
||||
var id = string.substr(offset, 4);
|
||||
chunks[id] = chunks[id] || [];
|
||||
if (id == 'RIFF' || id == 'LIST') {
|
||||
var len = parseInt(string.substr(offset + 4, 4).split('').map(function(i){
|
||||
var unpadded = i.charCodeAt(0).toString(2);
|
||||
return (new Array(8 - unpadded.length + 1)).join('0') + unpadded
|
||||
}).join(''),2);
|
||||
var data = string.substr(offset + 4 + 4, len);
|
||||
offset += 4 + 4 + len;
|
||||
chunks[id].push(parseRIFF(data));
|
||||
} else if (id == 'WEBP') {
|
||||
// Use (offset + 8) to skip past "VP8 "/"VP8L"/"VP8X" field after "WEBP"
|
||||
chunks[id].push(string.substr(offset + 8));
|
||||
offset = string.length;
|
||||
} else {
|
||||
// Unknown chunk type; push entire payload
|
||||
chunks[id].push(string.substr(offset + 4));
|
||||
offset = string.length;
|
||||
}
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
// here's a little utility function that acts as a utility for other functions
|
||||
// basically, the only purpose is for encoding "Duration", which is encoded as
|
||||
// a double (considerably more difficult to encode than an integer)
|
||||
function doubleToString(num){
|
||||
return [].slice.call(
|
||||
new Uint8Array(
|
||||
(
|
||||
new Float64Array([num]) //create a float64 array
|
||||
).buffer) //extract the array buffer
|
||||
, 0) // convert the Uint8Array into a regular array
|
||||
.map(function(e){ //since it's a regular array, we can now use map
|
||||
return String.fromCharCode(e) // encode all the bytes individually
|
||||
})
|
||||
.reverse() //correct the byte endianness (assume it's little endian for now)
|
||||
.join('') // join the bytes in holy matrimony as a string
|
||||
}
|
||||
|
||||
function WhammyVideo(speed, quality){ // a more abstract-ish API
|
||||
this.frames = [];
|
||||
this.duration = 1000 / speed;
|
||||
this.quality = quality || 0.8;
|
||||
}
|
||||
|
||||
WhammyVideo.prototype.add = function(frame, duration){
|
||||
if(typeof duration != 'undefined' && this.duration) throw "you can't pass a duration if the fps is set";
|
||||
if(typeof duration == 'undefined' && !this.duration) throw "if you don't have the fps set, you need to have durations here.";
|
||||
if(frame.canvas){ //CanvasRenderingContext2D
|
||||
frame = frame.canvas;
|
||||
}
|
||||
if(frame.toDataURL){
|
||||
// frame = frame.toDataURL('image/webp', this.quality);
|
||||
// quickly store image data so we don't block cpu. encode in compile method.
|
||||
frame = frame.getContext('2d').getImageData(0, 0, frame.width, frame.height);
|
||||
}else if(typeof frame != "string"){
|
||||
throw "frame must be a a HTMLCanvasElement, a CanvasRenderingContext2D or a DataURI formatted string"
|
||||
}
|
||||
if (typeof frame === "string" && !(/^data:image\/webp;base64,/ig).test(frame)) {
|
||||
throw "Input must be formatted properly as a base64 encoded DataURI of type image/webp";
|
||||
}
|
||||
this.frames.push({
|
||||
image: frame,
|
||||
duration: duration || this.duration
|
||||
});
|
||||
};
|
||||
|
||||
// deferred webp encoding. Draws image data to canvas, then encodes as dataUrl
|
||||
WhammyVideo.prototype.encodeFrames = function(callback){
|
||||
|
||||
if(this.frames[0].image instanceof ImageData){
|
||||
|
||||
var frames = this.frames;
|
||||
var tmpCanvas = document.createElement('canvas');
|
||||
var tmpContext = tmpCanvas.getContext('2d');
|
||||
tmpCanvas.width = this.frames[0].image.width;
|
||||
tmpCanvas.height = this.frames[0].image.height;
|
||||
|
||||
var encodeFrame = function(index){
|
||||
console.log('encodeFrame', index);
|
||||
var frame = frames[index];
|
||||
tmpContext.putImageData(frame.image, 0, 0);
|
||||
frame.image = tmpCanvas.toDataURL('image/webp', this.quality);
|
||||
if(index < frames.length-1){
|
||||
setTimeout(function(){ encodeFrame(index + 1); }, 1);
|
||||
}else{
|
||||
callback();
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
encodeFrame(0);
|
||||
}else{
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
WhammyVideo.prototype.compile = function(outputAsArray, callback){
|
||||
|
||||
this.encodeFrames(function(){
|
||||
|
||||
var webm = new toWebM(this.frames.map(function(frame){
|
||||
var webp = parseWebP(parseRIFF(atob(frame.image.slice(23))));
|
||||
webp.duration = frame.duration;
|
||||
return webp;
|
||||
}), outputAsArray);
|
||||
callback(webm);
|
||||
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
return {
|
||||
Video: WhammyVideo,
|
||||
fromImageArray: function(images, fps, outputAsArray){
|
||||
return toWebM(images.map(function(image){
|
||||
var webp = parseWebP(parseRIFF(atob(image.slice(23))))
|
||||
webp.duration = 1000 / fps;
|
||||
return webp;
|
||||
}), outputAsArray)
|
||||
},
|
||||
toWebM: toWebM
|
||||
// expose methods of madness
|
||||
}
|
||||
})()
|
||||
Loading…
Reference in New Issue