transform tool

This commit is contained in:
Skyler Lehmkuhl 2024-12-04 16:21:55 -05:00
parent c66f84c1ed
commit 016e8148ed
6 changed files with 5589 additions and 40 deletions

2
src/CCapture.all.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1214
src/canvas2svg.js Normal file

File diff suppressed because it is too large Load Diff

3537
src/ffmpeg-mp4.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -113,6 +113,7 @@ let context = {
selectionRect: undefined, selectionRect: undefined,
selection: [], selection: [],
shapeselection: [], shapeselection: [],
dragDirection: undefined,
} }
let config = { let config = {
@ -316,8 +317,6 @@ let actions = {
}, },
execute: (action) => { execute: (action) => {
let frame = pointerList[action.frame] let frame = pointerList[action.frame]
console.log(pointerList)
console.log(action.frame)
frame.keys = structuredClone(action.newState) frame.keys = structuredClone(action.newState)
}, },
rollback: (action) => { rollback: (action) => {
@ -368,7 +367,6 @@ let actions = {
} else if (layer.frames[frameNum].frameType != "keyframe") { } else if (layer.frames[frameNum].frameType != "keyframe") {
formerType = layer.frames[frameNum].frameType formerType = layer.frames[frameNum].frameType
} else { } else {
console.log("foolish")
return // Already a keyframe, nothing to do return // Already a keyframe, nothing to do
} }
redoStack.length = 0 redoStack.length = 0
@ -1200,7 +1198,8 @@ class GraphicsObject {
this.x = 0; this.x = 0;
this.y = 0; this.y = 0;
this.rotation = 0; // in radians this.rotation = 0; // in radians
this.scale = 1; this.scale_x = 1;
this.scale_y = 1;
if (!uuid) { if (!uuid) {
this.idx = uuidv4() this.idx = uuidv4()
} else { } else {
@ -1234,7 +1233,6 @@ class GraphicsObject {
} else if (this.activeLayer.frames[num].frameType == "motion") { } else if (this.activeLayer.frames[num].frameType == "motion") {
let frameKeys = {} let frameKeys = {}
const t = (num - this.activeLayer.frames[num].prevIndex) / (this.activeLayer.frames[num].nextIndex - this.activeLayer.frames[num].prevIndex); 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) { for (let key in this.activeLayer.frames[num].prev.keys) {
frameKeys[key] = {} frameKeys[key] = {}
let prevKeyDict = this.activeLayer.frames[num].prev.keys[key] let prevKeyDict = this.activeLayer.frames[num].prev.keys[key]
@ -1287,17 +1285,19 @@ class GraphicsObject {
growBoundingBox(bbox, child.bbox()) growBoundingBox(bbox, child.bbox())
} }
} }
bbox.x.max *= this.scale_x
bbox.y.max *= this.scale_y
bbox.x.min += this.x bbox.x.min += this.x
bbox.x.max += this.x bbox.x.max += this.x
bbox.y.min += this.y bbox.y.min += this.y
bbox.y.max += this.y bbox.y.max += this.y
console.log(bbox)
return bbox return bbox
} }
draw(context) { draw(context) {
let ctx = context.ctx; let ctx = context.ctx;
ctx.translate(this.x, this.y) ctx.translate(this.x, this.y)
ctx.rotate(this.rotation) ctx.rotate(this.rotation)
ctx.scale(this.scale_x, this.scale_y)
// if (this.currentFrameNum>=this.maxFrame) { // if (this.currentFrameNum>=this.maxFrame) {
// this.currentFrameNum = 0; // this.currentFrameNum = 0;
// } // }
@ -1316,7 +1316,8 @@ class GraphicsObject {
child.x = this.currentFrame.keys[idx].x; child.x = this.currentFrame.keys[idx].x;
child.y = this.currentFrame.keys[idx].y; child.y = this.currentFrame.keys[idx].y;
child.rotation = this.currentFrame.keys[idx].rotation; 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() ctx.save()
child.draw(context) child.draw(context)
if (true) { if (true) {
@ -1363,28 +1364,66 @@ class GraphicsObject {
ctx.fill() ctx.fill()
ctx.restore() ctx.restore()
} }
for (let item of context.selection) { if (mode == "select") {
ctx.save() for (let item of context.selection) {
ctx.strokeStyle = "#00ffff" ctx.save()
ctx.lineWidth = 1; ctx.strokeStyle = "#00ffff"
ctx.beginPath() ctx.lineWidth = 1;
let bbox = item.bbox() ctx.beginPath()
ctx.rect(bbox.x.min, bbox.y.min, bbox.x.max - bbox.x.min, bbox.y.max - bbox.y.min) let bbox = item.bbox()
ctx.stroke() ctx.rect(bbox.x.min, bbox.y.min, bbox.x.max - bbox.x.min, bbox.y.max - bbox.y.min)
ctx.restore() ctx.stroke()
} ctx.restore()
if (context.selectionRect) { }
ctx.save() if (context.selectionRect) {
ctx.strokeStyle = "#00ffff" ctx.save()
ctx.lineWidth = 1; ctx.strokeStyle = "#00ffff"
ctx.beginPath() ctx.lineWidth = 1;
ctx.rect( ctx.beginPath()
context.selectionRect.x1, context.selectionRect.y1, ctx.rect(
context.selectionRect.x2 - context.selectionRect.x1, context.selectionRect.x1, context.selectionRect.y1,
context.selectionRect.y2 - context.selectionRect.y1 context.selectionRect.x2 - context.selectionRect.x1,
) context.selectionRect.y2 - context.selectionRect.y1
ctx.stroke() )
ctx.restore() 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, x: x,
y: y, y: y,
rotation: 0, rotation: 0,
scale: 1, scale_x: 1,
scale_y: 1,
} }
} }
removeShape(shape) { removeShape(shape) {
@ -1659,7 +1699,6 @@ function addFrame() {
} }
function addKeyframe() { function addKeyframe() {
console.log(context.activeObject.currentFrameNum)
actions.addKeyframe.create() actions.addKeyframe.create()
} }
@ -1737,10 +1776,56 @@ async function render() {
function stage() { function stage() {
let stage = document.createElement("canvas") let stage = document.createElement("canvas")
let scroller = document.createElement("div") let scroller = document.createElement("div")
let stageWrapper = document.createElement("div")
stage.className = "stage" stage.className = "stage"
stage.width = 1500 stage.width = 1500
stage.height = 1000 stage.height = 1000
scroller.className = "scroll" 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) => { stage.addEventListener("drop", (e) => {
e.preventDefault() e.preventDefault()
let mouse = getMousePos(stage, e) let mouse = getMousePos(stage, e)
@ -1784,7 +1869,9 @@ function stage() {
e.preventDefault() e.preventDefault()
}) })
canvases.push(stage) canvases.push(stage)
scroller.appendChild(stage) stageWrapper.appendChild(stage)
stageWrapper.appendChild(selectionRect)
scroller.appendChild(stageWrapper)
stage.addEventListener("mousedown", (e) => { stage.addEventListener("mousedown", (e) => {
let mouse = getMousePos(stage, e) let mouse = getMousePos(stage, e)
switch (mode) { switch (mode) {
@ -1884,6 +1971,7 @@ function stage() {
stage.addEventListener("mouseup", (e) => { stage.addEventListener("mouseup", (e) => {
context.mouseDown = false context.mouseDown = false
context.dragging = false context.dragging = false
context.dragDirection = undefined
context.selectionRect = undefined context.selectionRect = undefined
let mouse = getMousePos(stage, e) let mouse = getMousePos(stage, e)
switch (mode) { switch (mode) {
@ -2038,6 +2126,40 @@ function stage() {
} }
context.lastMouse = mouse context.lastMouse = mouse
break; 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: default:
break; break;
} }
@ -2060,6 +2182,7 @@ function toolbar() {
toolbtn.addEventListener("click", () => { toolbtn.addEventListener("click", () => {
mode = tool mode = tool
updateInfopanel() updateInfopanel()
updateUI()
console.log(tool) console.log(tool)
}) })
} }
@ -2267,7 +2390,6 @@ function splitPane(div, percent, horiz, newPane=undefined) {
const frac = getMousePositionFraction(event, event.currentTarget) const frac = getMousePositionFraction(event, event.currentTarget)
div.setAttribute("lb-percent", frac*100) div.setAttribute("lb-percent", frac*100)
updateAll() updateAll()
console.log(frac); // Ensure the fraction is between 0 and 1
} }
}); });
div.addEventListener('mouseup', (event) => { 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() { function updateLayers() {
console.log(document.querySelectorAll(".layers-container"))
for (let container of document.querySelectorAll(".layers-container")) { for (let container of document.querySelectorAll(".layers-container")) {
let layerspanel = container.querySelectorAll(".layers")[0] let layerspanel = container.querySelectorAll(".layers")[0]
let framescontainer = container.querySelectorAll(".frames-container")[0] let framescontainer = container.querySelectorAll(".frames-container")[0]
layerspanel.textContent = "" layerspanel.textContent = ""
framescontainer.textContent = "" framescontainer.textContent = ""
console.log(context.activeObject)
for (let layer of context.activeObject.layers) { for (let layer of context.activeObject.layers) {
let layerHeader = document.createElement("div") let layerHeader = document.createElement("div")
layerHeader.className = "layer-header" layerHeader.className = "layer-header"
@ -2347,11 +2490,9 @@ function updateLayers() {
layerTrack.className = "layer-track" layerTrack.className = "layer-track"
framescontainer.appendChild(layerTrack) framescontainer.appendChild(layerTrack)
layerTrack.addEventListener("click", (e) => { layerTrack.addEventListener("click", (e) => {
console.log(layerTrack.getBoundingClientRect())
let mouse = getMousePos(layerTrack, e) let mouse = getMousePos(layerTrack, e)
let frameNum = parseInt(mouse.x/25) let frameNum = parseInt(mouse.x/25)
context.activeObject.currentFrameNum = frameNum context.activeObject.currentFrameNum = frameNum
console.log(context.activeObject )
updateLayers() updateLayers()
updateMenu() updateMenu()
updateUI() updateUI()

View File

@ -4,6 +4,10 @@ body {
overflow: hidden; overflow: hidden;
} }
* {
user-select: none;
}
.logo.vanilla:hover { .logo.vanilla:hover {
filter: drop-shadow(0 0 2em #ffe21c); filter: drop-shadow(0 0 2em #ffe21c);
} }
@ -174,7 +178,85 @@ button {
.stage { .stage {
width: 1500px; width: 1500px;
height: 1000px; 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 { .toolbar {
display: flex; display: flex;

573
src/whammy.js Normal file
View File

@ -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
}
})()